diff --git a/src/components/ava/header.tsx b/src/components/ava/header.tsx
index e7bbe99..29c3c60 100755
--- a/src/components/ava/header.tsx
+++ b/src/components/ava/header.tsx
@@ -12,6 +12,7 @@ import { Button } from '@/components/ui/button'
import { Switch } from '@/components/ui/switch'
import { BeatDot } from '@/components/ava/beat-dot'
import { LinkDialog } from '@/components/ava/link-dialog'
+import { WhatsNew } from '@/components/ava/whats-new-dialog'
import { tapBeat, useAudioSource } from '@/components/ava/audio-source'
import { useAvaStore } from '@/lib/store'
import { getStoredDarkBase } from '@/lib/theme'
@@ -191,8 +192,9 @@ export function AppHeader() {
- {/* theme switch + BPM readout + beat indicator + Set Beat — always visible */}
+ {/* what's new + theme switch + BPM readout + beat indicator + Set Beat — always visible */}
+
diff --git a/src/components/ava/whats-new-dialog.tsx b/src/components/ava/whats-new-dialog.tsx
new file mode 100644
index 0000000..d60b0c1
--- /dev/null
+++ b/src/components/ava/whats-new-dialog.tsx
@@ -0,0 +1,159 @@
+// OVE — "What's New!" header button + dialog (v7.6).
+//
+// Self-contained: the button and its dialog live together (local open state,
+// no store wiring needed — nothing else opens this dialog). The list comes
+// from src/lib/release-notes.ts, newest release first; the running version
+// is badged "current". A small emerald dot on the button marks updates the
+// visitor hasn't seen yet — stored per visitor in localStorage and cleared
+// the moment the dialog is opened (mount-guarded so SSR never mismatches,
+// same pattern as the header's ThemeToggle).
+
+'use client'
+
+import { useState, useSyncExternalStore } from 'react'
+import { Sparkles } from 'lucide-react'
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog'
+import { RELEASE_NOTES, type ChangeKind, type ReleaseNote } from '@/lib/release-notes'
+import { APP_SHORT, APP_VERSION } from '@/lib/version'
+import { thinScrollbar } from '@/components/ava/shared'
+import { cn } from '@/lib/utils'
+
+const SEEN_KEY = 'ove.whatsNew.seen'
+
+const KIND_META: Record = {
+ added: { label: 'Added', cls: 'border-emerald-400/40 bg-emerald-400/10 text-emerald-400' },
+ improved: { label: 'Improved', cls: 'border-amber-400/40 bg-amber-400/10 text-amber-400' },
+ fixed: { label: 'Fixed', cls: 'border-rose-400/40 bg-rose-400/10 text-rose-400' },
+ removed: { label: 'Removed', cls: 'border-zinc-500/40 bg-zinc-500/10 text-zinc-400' },
+}
+
+/** One release: version chip + name + change rows with kind tags. */
+function ReleaseSection({ note, current }: { note: ReleaseNote; current: boolean }) {
+ return (
+
+
+
+ )
+}
+
+const emptySubscribe = () => () => {}
+/** Hydration-safe localStorage read: null on the server, the stored value on
+ * the client after hydration (React re-checks the snapshot on every render,
+ * so writing the key then re-rendering updates it — no effects needed). */
+function useSeenVersion(): string | null {
+ return useSyncExternalStore(
+ emptySubscribe,
+ () => {
+ try {
+ return localStorage.getItem(SEEN_KEY) ?? ''
+ } catch {
+ return ''
+ }
+ },
+ () => null,
+ )
+}
+
+export function WhatsNew() {
+ const [open, setOpen] = useState(false)
+ const seenVersion = useSeenVersion()
+ const unseen = seenVersion !== null && seenVersion !== APP_VERSION
+
+ const handleOpenChange = (o: boolean) => {
+ setOpen(o)
+ if (o) {
+ try {
+ localStorage.setItem(SEEN_KEY, APP_VERSION)
+ } catch {
+ /* private mode — the dot just stays for this visit */
+ }
+ }
+ }
+
+ return (
+ <>
+
+
+
+ >
+ )
+}
diff --git a/src/lib/release-notes.ts b/src/lib/release-notes.ts
new file mode 100644
index 0000000..8c06301
--- /dev/null
+++ b/src/lib/release-notes.ts
@@ -0,0 +1,219 @@
+// OVE — release notes for the "What's New!" dialog (v7.6).
+//
+// The visitor-facing changelog, newest release first. This is the ONE place
+// to update when shipping a version: every bump of APP_VERSION in version.ts
+// must come with a matching entry at the top of RELEASE_NOTES (a dev-mode
+// guard below warns in the console when the two drift apart).
+//
+// Keep entries brief — one line per change, written for visitors (what they
+// can now do / what stopped breaking), not for the build log. The verbose
+// engineering history still lives in version.ts; this file is the digest.
+//
+// Change kinds shown in the dialog:
+// added — new capability
+// improved — made better/faster/ friendlier
+// fixed — a bug that stopped reproducing
+// removed — taken away (with the reason in the text)
+
+import { APP_VERSION } from '@/lib/version'
+
+export type ChangeKind = 'added' | 'improved' | 'fixed' | 'removed'
+
+export interface ReleaseChange {
+ kind: ChangeKind
+ text: string
+}
+
+export interface ReleaseNote {
+ /** matches APP_VERSION of that release, e.g. '7.5.1' */
+ version: string
+ /** short release name shown next to the version (optional) */
+ name?: string
+ changes: ReleaseChange[]
+}
+
+export const RELEASE_NOTES: ReleaseNote[] = [
+ {
+ version: '7.6',
+ name: 'What’s New!',
+ changes: [
+ { kind: 'added', text: '“What’s New!” button in the header — every release’s additions, improvements and fixes at a glance.' },
+ { kind: 'added', text: 'A small dot on the button marks updates you haven’t seen yet; opening the list clears it.' },
+ ],
+ },
+ {
+ version: '7.5.1',
+ changes: [
+ { kind: 'fixed', text: 'The “Stream a music link” dialog no longer stretches past its own border when a saved link title is long.' },
+ { kind: 'improved', text: 'The link dialog is a little wider, and long titles end with an ellipsis instead of overflowing.' },
+ ],
+ },
+ {
+ version: '7.5',
+ name: 'Music links that just play',
+ changes: [
+ { kind: 'fixed', text: 'Many YouTube links failing with “media resource not suitable” — tracks are now downloaded audio-only and converted to MP3 on the server, instead of streaming expiring, IP-locked URLs.' },
+ { kind: 'improved', text: 'The Playing label shows the video’s real title.' },
+ { kind: 'improved', text: 'Over-long tracks get an honest “too long” answer, and playlist transitions no longer stall behind conversions.' },
+ ],
+ },
+ {
+ version: '7.4',
+ name: 'Sequence mode',
+ changes: [
+ { kind: 'added', text: '“Sequence” mix mode — cycles the folder’s presets in order, wrapping back to the first so the folder loops forever.' },
+ { kind: 'improved', text: 'Random never picks the same preset twice in a row; the demo showcase now walks all ten Zai-Magic presets in order.' },
+ ],
+ },
+ {
+ version: '7.3',
+ name: 'Demo-flow refinements',
+ changes: [
+ { kind: 'improved', text: 'The demo track plays once through and returns you to the source picker — a demo, not a radio station.' },
+ { kind: 'fixed', text: 'The randomizer no longer switches presets while no music is playing.' },
+ { kind: 'removed', text: 'Preset-change popups (“Preset loaded”…) — the footer’s SCRIPT label already echoes every change.' },
+ ],
+ },
+ {
+ version: '7.2.1',
+ changes: [
+ { kind: 'fixed', text: 'Zai-Magic “Supernova” fading to black after its first explosion — every beat now re-detonates the field with fresh geometry.' },
+ ],
+ },
+ {
+ version: '7.2',
+ name: 'Showcase flow',
+ changes: [
+ { kind: 'added', text: 'Animated shimmer on the Demo Track button — one click starts the full showcase (demo music + the Zai-Magic folder on random mix).' },
+ { kind: 'added', text: 'Preset changes blur-dissolve over 0.7 s instead of hard-cutting.' },
+ { kind: 'improved', text: 'Fresh visitors start on the Black + Purple Outline theme with Bass Bloom loaded.' },
+ ],
+ },
+ {
+ version: '7.1',
+ name: 'Real demo track + beat detector rewrite',
+ changes: [
+ { kind: 'added', text: 'The Demo button plays “Neural Mecha Pulse” — a real mastered recording — with live tempo detection and a track chip in the header.' },
+ { kind: 'improved', text: 'Beat detection rewritten the way real trackers work (spectral flux → autocorrelation → phase-locked grid): locks the true BPM of real music in about two seconds.' },
+ { kind: 'removed', text: 'The demo BPM slider — a real track’s tempo is detected, not dialed in.' },
+ ],
+ },
+ {
+ version: '7.00',
+ name: 'Zai-Magic',
+ changes: [
+ { kind: 'added', text: 'Ten hand-authored presets built around beat-synced color — the color wheel turns on the kick, palettes swap per beat and per bar.' },
+ { kind: 'added', text: 'Script tools hsvr/hsvg/hsvb, smoothstep and the beatage kick-envelope variable, documented in the help panel.' },
+ ],
+ },
+ {
+ version: '6.9.3',
+ changes: [{ kind: 'fixed', text: 'Share URLs point at the correct domain.' }],
+ },
+ {
+ version: '6.9.2',
+ changes: [{ kind: 'fixed', text: 'The share preview image now passes Facebook’s checks; new title-card artwork.' }],
+ },
+ {
+ version: '6.9.1',
+ changes: [
+ { kind: 'added', text: 'Social share previews — sharing the site shows the big image card on Facebook, X, WhatsApp and Discord.' },
+ ],
+ },
+ {
+ version: '6.9',
+ name: 'House account',
+ changes: [
+ { kind: 'added', text: 'Anonymous visitors play YouTube links through the shared house account — bot walls stop applying to guests.' },
+ ],
+ },
+ {
+ version: '6.8.2',
+ changes: [
+ { kind: 'improved', text: 'Music-service login cut to about four clicks — drag the exported cookie file straight onto the login box.' },
+ ],
+ },
+ {
+ version: '6.8.1',
+ changes: [
+ { kind: 'improved', text: 'The login box recognizes raw Cookie headers, cookies.txt and Cookie-Editor JSON, and validates them live.' },
+ ],
+ },
+ {
+ version: '6.8',
+ name: 'Self-healing engine',
+ changes: [
+ { kind: 'added', text: 'When YouTube changes their mechanics, the server updates yt-dlp itself and retries behind an “Updating in Background” notice.' },
+ ],
+ },
+ {
+ version: '6.7',
+ name: 'Lazy playlist queue',
+ changes: [
+ { kind: 'added', text: 'Playlist links become a queue that streams one song at a time — each track is fetched only right before it plays.' },
+ { kind: 'added', text: 'Queue bar with skip, jump and replay.' },
+ ],
+ },
+ {
+ version: '6.6',
+ name: 'Music service logins',
+ changes: [
+ { kind: 'added', text: 'Settings-tab logins for YouTube, SoundCloud, Spotify and Apple Music, encrypted on your computer.' },
+ ],
+ },
+ {
+ version: '6.5',
+ name: 'Settings tab & themes',
+ changes: [{ kind: 'added', text: 'Theme picker — base chrome × highlight color, saved per visitor.' }],
+ },
+ {
+ version: '6.4',
+ changes: [
+ { kind: 'fixed', text: 'Beat detection suspends when no music is playing — no ghost beats after Stop.' },
+ { kind: 'added', text: 'Royal purple dark theme (later joined by more bases).' },
+ ],
+ },
+ {
+ version: '6.3',
+ changes: [{ kind: 'added', text: 'Light/dark tick switch in the header.' }],
+ },
+ {
+ version: '6.2',
+ changes: [
+ { kind: 'improved', text: 'The embedded preset library is read-only; your own presets live in your browser.' },
+ ],
+ },
+ {
+ version: '6.1',
+ changes: [
+ { kind: 'improved', text: 'Every embedded preset is a unique generated visual — sixteen families, seeded parameters.' },
+ ],
+ },
+ {
+ version: '6.0',
+ changes: [
+ { kind: 'improved', text: 'Renamed to Open Visualization Engine, with a version stamp in the footer.' },
+ ],
+ },
+ {
+ version: '5.0',
+ changes: [{ kind: 'added', text: 'Folder-tree Presets page.' }],
+ },
+ {
+ version: '4.0',
+ changes: [{ kind: 'added', text: 'Link audio source — paste a URL, hear the music.' }],
+ },
+]
+
+// Dev-mode guard: the newest entry must describe the running version. If you
+// bumped APP_VERSION without adding its release note, this warns in the
+// browser console AND the server log so it gets noticed before shipping.
+if (
+ process.env.NODE_ENV !== 'production' &&
+ RELEASE_NOTES.length > 0 &&
+ RELEASE_NOTES[0].version !== APP_VERSION
+) {
+ console.warn(
+ `[release-notes] RELEASE_NOTES[0] (v${RELEASE_NOTES[0].version}) does not match APP_VERSION (v${APP_VERSION}) — add the new release's entry to src/lib/release-notes.ts.`,
+ )
+}
diff --git a/src/lib/version.ts b/src/lib/version.ts
index fd1f635..9cec5c8 100755
--- a/src/lib/version.ts
+++ b/src/lib/version.ts
@@ -6,7 +6,22 @@
// useful both for the z.ai Version History and for naming local backups
// (v6.0 = rename to Open Visualization Engine + version indicator).
//
+// STANDING RULE (since v7.6): every APP_VERSION bump MUST come with a
+// matching entry at the top of RELEASE_NOTES in src/lib/release-notes.ts —
+// that list powers the header's "What's New!" dialog. A dev-mode guard
+// warns in the console when the two drift apart.
+//
// History:
+// 7.6 — "What's New!" dialog: a Sparkles button in the header (left of
+// the theme switch) opens the visitor-facing changelog — every
+// release from v4.0 on, newest first, each with one-line Added /
+// Improved / Fixed / Removed notes (src/lib/release-notes.ts,
+// color-coded tags, the running version badged "current",
+// scrollable with the thin scrollbar). A small emerald dot on the
+// button marks updates the visitor hasn't seen yet — per visitor
+// in localStorage, cleared on open (mount-guarded, no hydration
+// mismatch). Standing rule added: version bumps ship with their
+// release note; a dev-mode guard warns when they drift
// 7.5.1 — Link dialog width fix: text no longer escapes the dialog box.
// Root cause: Radix dialogs lay their children in an implicit CSS
// grid whose single column sizes to the widest child's min-content;
@@ -205,4 +220,4 @@
export const APP_NAME = 'Open Visualization Engine'
export const APP_SHORT = 'OVE'
-export const APP_VERSION = '7.5.1'
+export const APP_VERSION = '7.6'
diff --git a/worklog.md b/worklog.md
index 530e0e7..d3fecc2 100755
--- a/worklog.md
+++ b/worklog.md
@@ -2139,3 +2139,25 @@ E2E (agent-browser, ALL PASS):
Stage Summary:
- v7.5.1 shipped: the Link dialog (and every other dialog) can no longer be stretched past its border by long unbreakable content — the shared dialog column is capped at the available width, the Link dialog is comfortably wider (512 px), long recent/playlist titles truncate with ellipsis, and error text with embedded URLs wraps. Deploy note: no server changes, pure client fix.
+
+---
+Task ID: 34
+Agent: main (Z.ai Code)
+Task: v7.6 "What's New!" (user request): a button to the left of the header's dark/light theme switch opens a dialog titled "What's New!" listing every release version with a brief summary of what was added / improved / fixed / removed — populated with the project's full history (v4.0 on), and from now on every version bump must add its entry here too.
+
+Work Log:
+- src/lib/release-notes.ts (new): the visitor-facing changelog data — RELEASE_NOTES newest-first (26 entries, v4.0 → v7.6), each { version, name?, changes[] } with changes typed added/improved/fixed/removed and written as one-line visitor-oriented bullets (digest of the verbose version.ts history; engineering detail stays there). Module-scope dev guard warns in the console + dev.log when RELEASE_NOTES[0].version drifts from APP_VERSION, enforcing the new standing rule.
+- src/components/ava/whats-new-dialog.tsx (new): self-contained — Sparkles button (h-11 bordered group styling to match the header, emerald icon, "What's New!" label, icon-only text on xs) + Dialog. Dialog: title "What's New!" with sparkle icon, description "Every update shipped to OVE — N releases and counting, newest first", scrollable list (max-h min(60vh,520px), thinScrollbar), each release = version chip (mono, bordered) + optional name + change rows with color-coded ADDED/IMPROVED/FIXED/REMOVED tags (emerald/amber/rose/zinc — all remapped per theme base & accent, verified in globals.css); the running APP_VERSION gets an emerald "current" badge. Unseen-updates dot: small emerald dot on the button when the visitor's last-opened version (localStorage ove.whatsNew.seen) ≠ APP_VERSION; opening the dialog stores the current version and clears the dot. Read via useSyncExternalStore (server snapshot null → no hydration mismatch, client snapshot reads localStorage, React re-checks the snapshot on the setOpen re-render so the dot clears without effects — the react-hooks/set-state-in-effect lint rule blocked the useEffect version).
+- header.tsx: mounted in the always-visible right group, first item (left of ThemeToggle as requested).
+- version.ts: APP_VERSION 7.6 + history entry + STANDING RULE comment (every bump ships with its RELEASE_NOTES entry). lint clean; tsc clean (pre-existing examples/skills only).
+
+E2E (agent-browser + VLM, ALL PASS):
+- Button sits left of the theme switch (btn right 952 < switch left 995), unseen dot present on first visit; title now "OVE — Open Visualization Engine (v7.6)".
+- Dialog: title "What's New!" + sparkle; 26 releases newest-first; v7.6 first with "current" badge; scrollable (2622 px content in 346 px pane; scrolled to bottom → v5.0/v4.0 visible); ZERO elements outside the dialog box (v7.5.1 minmax(0,1fr) fix covers this dialog too); 512 px wide desktop / 343 px at 375 px viewport, fits.
+- VLM (dark): tags colored correctly (ADDED green, IMPROVED amber, FIXED rose), no overflow, no cut-off text, aligned. VLM (light theme via the tick switch): all tags/chips/body text readable on white, no contrast problems.
+- Dot lifecycle: fresh visit → dot; open → stored 7.6, dot gone after close; simulated old visitor (seen=7.5) → dot returns after reload, clears on open.
+- Header widths: 375/390/414/768 px zero overflow with the new button. ≤360 px the header overflows IDENTICALLY with the button hidden (pre-existing BPM/BeatDot/Set-Beat row issue, not a regression — baseline parity measured).
+- No release-notes drift warning in console or dev.log (guard green); zero page errors; dev.log clean.
+
+Stage Summary:
+- v7.6 shipped: the header now carries a "What's New!" button (left of the theme switch) opening the full release-notes dialog — 26 releases from v4.0 to today, tagged Added/Improved/Fixed/Removed, newest first, current version badged, with an unseen-updates dot. The release-notes module is now part of the version-bump workflow (standing rule + dev-mode drift guard).