Changelog
All notable changes to XOSC. Newest first. Updated automatically by just package.
Unreleased
- _nothing yet_
v0.7.19 — 2026-05-01
- Fix: enabling input "global capture" no longer blanks the screen.
useGlobalHotkeysselector returned a fresh{signature, accelerator, descriptor}object per node on every snapshot.useShallowcompares array elements withObject.is— always false for new object refs — so getSnapshot churned indefinitely the moment any node hadglobalHotkey: true, and React 18's stale-snapshot detector aborted the renderer with #185 "Maximum update depth exceeded". The selector now returns a flatsignature\tacceleratorstring array (primitives are shallow-stable when content matches); the wire payload is derived viauseMemo. Unuseddescriptorfield on the entry shape dropped. (src/features/inputs/useGlobalHotkeys.ts)
v0.7.18 — 2026-05-01
- Fix: first-launch splash hang on multi-monitor / mixed-DPI Windows.
BrowserWindow.ready-to-showsilently never fires (or fires too late) on some Windows GPU configurations — typically tri-monitor setups with mixed DPI scaling. The splash stayed up forever, the main window stayed hidden, and the user concluded the app was frozen. ThecreateMain()reveal logic is now an idempotentreveal(via, dwell)armed by three triggers: the originalready-to-show(1400 ms splash dwell preserved),did-finish-load(no dwell — the renderer is already loaded by the time it fires), and a 5 s safety timer (last resort). Newphase=main-window-reveal via=...lands in lifecycle.log so the diagnostic confirms which trigger won. Also confirms the v0.7.7→v0.7.15 "close-then-reopen crash" was always the same bug — the user task-killed a stuck splash, which lifecycle.log recorded asreason=killed exitCode=1. With v0.7.18 the splash dismisses cleanly and close-then-reopen ends with=== CLEAN-EXIT ===every run. (electron/main.ts)
v0.7.17 — 2026-05-01
- userData directory listing on every boot. Right after
main-entry, lifecycle.log gets aphase=userdata-listingentry followed by every file/folder in%APPDATA%\xosc\(relative path, size, mtime). Capped at 200 entries / 4 levels deep. Surfaces stale.tmp.<pid>.<ts>save orphans,.corrupt.<ts>quarantines, and Chromium caches (Local Storage, IndexedDB, Cookies) so the "presets stored somewhere I didn't expect" mystery becomes a one-grep job. (electron/lifecycle-log.ts,electron/main.ts)
v0.7.16 — 2026-05-01
- v0.7.16 diagnostic battery — instrument the renderer side of the silent kill. v0.7.15 proved main-process teardown is clean (every
dispose-ok, every=== CLEAN-EXIT ===); the kill is renderer-side. v0.7.16 ships four pieces of instrumentation so the next reproduction lands hard evidence:
%APPDATA%\xosc\heartbeat.log overwritten each beat. After a kill, the timestamp on disk + the stored counter tell us whether the renderer was healthy up to the moment of termination (sudden external kill) or already wedged for many seconds (slow hang). (src/heartbeat.ts, electron/lifecycle-log.ts)
- webContents event capture in main: every did-start-loading / did-finish-load / did-fail-load / dom-ready / unresponsive / responsive / render-process-gone is timestamped to lifecycle.log. unresponsive is also written to crash.log so it stands out — it's Chromium's "I'll TerminateProcess in 30 s if this doesn't recover" signal. Console messages at warn/error level are appended to crash.log so React #185 / xyflow loops show up post-mortem. (electron/main.ts)
- Renderer phase marks for entry, react-mount-ok, config-load-start, config-load-shape (with nodeCount + per-kind counts — shape only, never contents), config-hydrated, hydrated-flag-set, config-load-error. Lifecycle log is now end-to-end: main entry → renderer mount → config load → hydrate. The last line before a kill pinpoints the bad step. (src/main.tsx, src/features/settings/useConfigSync.ts, electron/main.ts)
- Top-bar ? menu adds "Open heartbeat log" so the user can grab the third diagnostic file alongside crash.log and lifecycle.log. (src/components/Topbar.tsx)
No behavioural change — pure observability. Once the next kill is reproduced, the three logs together should pinpoint the cause unambiguously.
v0.7.15 — 2026-05-01
- Lifecycle log added — silent, persistent boot/teardown timeline at
%APPDATA%\XOSC\lifecycle.log. Every meaningful main-process phase (single-instance lock, when-ready, splash + main window create, each handler register, before-quit, window-all-closed, each dispose call, will-quit, quit) is timestamped to disk. Each dispose is wrapped in a try/catch that recordsdispose-throwwith the error message, so a throw in one teardown step no longer short-circuits the rest. A=== CLEAN-EXIT ===footer is appended onapp.on("quit"); absence between two BOOT headers proves the previous run died mid-shutdown — the diagnostic the silent close-then-reopen crash needed. Top-bar?menu adds "Open lifecycle log". Rotates to.oldat 512 KB. (electron/lifecycle-log.ts,electron/main.ts,electron/preload.ts,src/components/Topbar.tsx,src/types/api.ts) - DEBUG_BUILD disabled for ship — release.yml no longer sets
DEBUG_BUILD: "1", so v0.7.15+ builds are normal production: minified, prod React, no auto-opening DevTools, ~3× lighter installer. The crash log + lifecycle log are now the diagnostic surface; the client doesn't see a debug surface anymore. (.github/workflows/release.yml) - Easy-Next simplified to clip-only — the v0.7.14 axis toggle (advance clip vs advance layer) is gone. Every press walks the chosen layer's clips left-to-right; the layer dropdown stays so the user picks WHICH layer. Two motions on one Resolume box = two ResolumeOut nodes pinned to different layers. Saved configs with
advance: "layer"are normalised back to"clip"on hydration. The phantom-extra-layer bug in layer-advance is gone with the code. (src/features/inspector/ResolumeOutInspector.tsx,src/features/mapping/useMappingRouter.ts,src/store/store.ts) - Auto-refresh composition when Easy-Next opens — opening the Easy-Next inspector section auto-fetches Resolume's composition tree if state is
idle, instead of requiring a manual click on "Refresh from Resolume" before the layer dropdown populates. Skips when already loading or in error state. (src/features/inspector/ResolumeOutInspector.tsx)
v0.7.14 — 2026-05-01 (diagnostic build, persistent crash log + Easy-Next redesigned around Resolume's grid)
Added — persistent crash log so we can finally see what's actually failing
%APPDATA%\XOSC\crash.logcaptures every uncaught exception, unhandled rejection, render-process-gone event, and renderer-side error/unhandledrejection. Wired before any other module load in both main and renderer (electron/crash-log.tssetup runs at the top ofelectron/main.ts;src/crash-handler.tsis the very first import insrc/main.tsx). SynchronousappendFileSyncso entries land even when the process is dying. The last six releases all rolled back blind because we had no diagnostic surface for "won't launch on Windows" failures — DevTools doesn't open if the renderer crashes before mount, and the app shows no on-screen feedback. This file is now the authoritative source.- "Open crash log" + "Show crash log folder" in the topbar
?menu. One click puts the file in front of the user. (src/components/Topbar.tsx) window.xosc.app.reportCrash/openCrashLog/showCrashLogFolderIPC wired through preload + types. Renderer'swindow.errorandunhandledrejectionlisteners forward to main; React mount throws are caught explicitly insrc/main.tsxand forwarded too. Buffered when the preload bridge isn't ready yet, flushed on next animation frame.
Changed — Easy-Next redesigned around Resolume's actual grid model
User insight: Resolume's coordinate system is ALWAYS (layer × column). There's no useful "advance column" without picking a layer first — server-side column-fan-out connects every layer's clip in that column simultaneously, which is rarely what you want. The actually-useful primitive is "I picked a layer, the button walks that layer's clips left to right". The complementary primitive is "stay on the same column, advance the layer dropdown" — useful for comparing the same beat across layers. Plus: Resolume renders layer N on top of layer N-1, so the array-order layer dropdown was visually inverted from what the user sees on stage.
- Mode selector (auto / columns / layer-clips) deleted. Always layer-clips. Saved configs from v0.7.7-v0.7.13 with
mode: "auto"or"columns"are normalised to"layer-clips"during hydration. (src/store/store.ts,src/features/inspector/ResolumeOutInspector.tsx) - New "Axis" toggle: clip-advance vs layer-advance. Clip-advance (default) walks columns within the chosen layer — same press, same layer, different clip. Layer-advance walks the layer dropdown while keeping the column position fixed — same press, next layer down/up, same beat. Both wrap. Implemented in the router as a new
advancefield onResolumeEasyNextSpec; the fire path branches on the axis and patches eithercurrentIndex(clip mode) orlayerIndex(layer mode). (src/store/store.ts,src/features/mapping/useMappingRouter.ts,src/features/inspector/ResolumeOutInspector.tsx) - Layer dropdown reversed. Resolume renders layer N on top of layer N-1, so array index 0 is the bottom of the visible stack. The dropdown now shows the visually top-most layer at the top of the list. Values stay 1-based to match Resolume's REST URLs. (
src/features/inspector/ResolumeOutInspector.tsx) - Layer-advance auto-skips empty layers. When the next layer in the cycle has no populated clips at all (or zero after
skipEmpty), the router skips it and tries the one after — caps at one full trip around so a totally-empty composition can't loop forever. (src/features/mapping/useMappingRouter.ts)
Notes for the next session
- v0.7.13 still crashed on close-then-reopen with a Resolume Out node configured, even after four-layer defence on
webContents.send. The crash log will surface the actual root cause on the next reproduction. Until that file shows real data, do not ship more behavioural changes — the fixes have to be evidence-driven now. - Still on
DEBUG_BUILD=1. Production cutover happens once the crash log shows a clean close-then-reopen cycle with a Resolume Out node.
v0.7.13 — 2026-05-01 (diagnostic build, Easy-Next regression revert + close-crash defence-in-depth)
Fixed (THIS is the "press goes down the wire but Resolume doesn't advance" regression I introduced in v0.7.11)
- Easy-Next router reverted to "next to fire" semantics. v0.7.11 changed
currentIndexto mean "currently playing" and made the router fire(connectedIdx + 1) % total. That works only if Resolume'sconnected.valueparses correctly. On the user's Resolume build it doesn't (clip likely inPreviewingstate, or shape mismatch), soconnectedIdx === -1, the router fell back tocurrentIndexfor fire BUT then setcurrentIndex = fireIdx(not+ 1). Result: every press fired the same slot. Resolume kept connecting the same clip. Visually dead. Restored v0.7.10's algorithm: always firecurrentIndex, always advance to(fireIdx + 1) % totalafter a successful send. The press now produces a different connect every time regardless ofconnectedparsing. (src/features/mapping/useMappingRouter.ts) - Inspector + node body now display
nextIdx(always known) as the primary indicator. The dashed-border "next press fires" row is the source of truth. Resolume's connected state is shown as a secondary(now: name)annotation when detectable, never as the primary highlight — guessing at the playhead whenconnectedwas unreadable was the v0.7.11 bug. (src/features/inspector/ResolumeOutInspector.tsx,src/features/mapping/ResolumeOutNode.tsx) - "Copy composition JSON" button added to the Easy-Next inspector. The user couldn't paste from DevTools (the composition is a few hundred KB) and a console
fetch()from the renderer hits CORS against Resolume's webserver. The button serializes the in-memory composition cache and writes it to the system clipboard vianavigator.clipboard, giving a one-click way to capture diagnostic data when something looks wrong. Removed once Easy-Next is stable. (src/features/inspector/ResolumeOutInspector.tsx)
Fixed (close-crash defence in depth — v0.7.12 fixed two sites; this catches the rest of the class)
Logger.pushnow wraps each listener in try/catch. v0.7.12 fixed twowebContents.sendcall sites (main.ts:194, serial-bridge.ts:54) that were re-throwing on shutdown. But the general problem is thatLogger.push.listeners.forEach(fn)had no per-listener insulation: a single listener throwing took out the rest of the dispose chain because the throw bubbled throughlog.emit→ bridge close handler →disposeResolumeBridges→app.on("window-all-closed"). Per-listener try/catch means a future listener regression can't ever soft-brick shutdown again. (electron/logger.ts)logger.beginShutdown()flag suppresses listener notifications afterapp.before-quit. Earliest signal that everything is tearing down. The logger still records entries in its in-memory buffer (useful if next-launch logs surface them), it just stops fanning them out to listeners that may have stale window references. (electron/logger.ts,electron/main.ts)resolume-bridge.ts pushStatusnow checkswc.isDestroyed()and try/catches the send. v0.7.12 audited this site and thought it was already guarded — it checkswin.isDestroyed()but notwin.webContents.isDestroyed(). Same race as the two sites fixed in v0.7.12. The above three changes mean each of the four "WS close fires during dispose" code paths is now triple-protected (per-listener try/catch in Logger + shuttingDown flag + per-call siteisDestroyed()checks + try/catch on send). (electron/resolume-bridge.ts)
Notes
- The user reported v0.7.12 still crashed on close (silently — no more error modal because the per-listener throw in v0.7.12's main.ts logger fix was working, but a different listener was throwing instead). v0.7.13's per-listener try/catch in Logger itself catches every shutdown listener regardless of which file it lives in.
- Still on diagnostic harness — production cutover happens once you confirm v0.7.13 (a) closes cleanly with a Resolume Out node, (b) advances Resolume on each press of an Easy-Next-enabled output, AND (c) the next launch boots without needing to delete
%APPDATA%\XOSC.
v0.7.12 — 2026-05-01 (diagnostic build, shutdown-crash root cause)
Fixed (THIS is the v0.7.7+ "won't open / crashes on close" root cause)
- Main process no longer throws "Object has been destroyed" during shutdown. The user's stack trace finally pinned it: when the window closes with a live Resolume WebSocket,
disposeResolumeBridges()runs in thewindow-all-closedhandler, the WScloseevent fires, the bridge callspushStatus→log.emit, the logger fans out to itsonChangesubscribers, and one of those subscribers (electron/main.ts:194) calledmainWin?.webContents.send("log:stream", …). The?.only guarded against null —mainWinitself was still a valid reference, but itswebContentshad already been torn down by Electron, so the send threw. Electron then surfaced the throw as the "A JavaScript error occurred in the main process" modal. Same race inelectron/serial-bridge.ts:54(parser data event firing after the window starts tearing down). Both sites now checkwin.isDestroyed()ANDwin.webContents.isDestroyed(), plus a try/catch swallows any remaining race, because log streaming + serial data are best-effort and re-throwing during graceful shutdown helps no one. (electron/main.ts,electron/serial-bridge.ts) - The "saved-config soft-brick" was a downstream symptom of the same crash. When the renderer threw the "Object has been destroyed" exception during shutdown, the
useConfigSyncdebounced save was sometimes mid-flight. With v0.7.11's atomic-renamewriteSnapshotthis is now harmless even if it does happen, but with v0.7.10's directfs.writeFilethe file landed truncated, then the next launch hitJSON.parse→ uncaught exception → frozen splash. v0.7.11 already shipped the atomic write + corrupt-file quarantine + try/catch hydration; v0.7.12 takes out the upstream cause. Both fixes stand: v0.7.11's hardening means a config-write crash from any future cause still recovers cleanly, and v0.7.12's IPC guard means the crash that triggered this whole investigation can't happen anymore.
Notes
- The bug class —
webContents.sendwithoutisDestroyedcheck — predates v0.7.6/v0.7.7. The reason it surfaced now: v0.7.7 added areadCachedCompositioncodepath that opens / refreshes Resolume WS connections more aggressively, and the user added a Resolume Out node to test Easy-Next, which is what made the WS close path hot during shutdown. v0.7.5 and earlier rarely triggered it because most users never had a connected WS at close time. Audited all otherwebContents.sendsites (dmx/engine.ts,osc-listener.ts,dmx/artnetIn.ts,peer-discovery.ts,global-hotkeys.ts,resolume-bridge.ts) — they already guard withisDestroyed()correctly. - Still on the diagnostic harness — production-mode build is one revert away once you confirm v0.7.12 launches AND closes cleanly with a Resolume Out node configured.
v0.7.11 — 2026-05-01 (diagnostic build, Easy-Next correctness fixes + soft-brick recovery)
Fixed (soft-brick recovery — root cause of v0.7.10 "won't reopen after JS error on close")
xosc.config.jsonis now written atomically.writeSnapshotpreviously calledfs.writeFiledirectly, so a renderer JS error mid-shutdown could leave a truncated JSON file on disk; the next launch'sJSON.parsethen threw anduseConfigSynccould not recover, soft-bricking the app until the user manually deleted%APPDATA%\XOSC. Switched to write-to-tmp + atomic-rename: a crash mid-write leaves either the previous good file or an orphaned.tmp.<pid>.<ts>, never a half-written real path. (electron/config-store.ts)- Corrupt config files self-quarantine on read instead of crashing the app. If
readSnapshothits an invalid-JSON or invalid-shape error, the bad file is renamed with a.corrupt.<ts>suffix and the app boots with the same empty snapshot it would on first launch — saved graph is preserved on disk for manual recovery, but the user gets a working app immediately. (electron/config-store.ts) hydrateFromSnapshotnow wrapped in try/catch with per-node sanitisation. Any unexpected throw inside hydration logs to console and falls through to defaults instead of bubbling out and stranding the renderer. Easy-Next blocks on saved nodes are normalised: invalidmodestrings collapse to"auto", NaN/negative/InfinitycurrentIndexcollapses to0, sub-1layerIndexis dropped. (src/store/store.ts)
Fixed
- Easy-Next playlist highlight now tracks Resolume's actual playhead, not the local pointer. Previously the highlighted item was always one ahead of what Resolume was playing because
currentIndexstored "next to fire" but the UI displayed it as "now playing". EachPlaylistItemnow carries aconnectedboolean derived from Resolume's per-clipconnected.value(mapped from both numeric and string-enum forms —Empty/Disconnected/Previewing/Connected/Connected & previewing→0..4, threshold>= 3); the inspector + node body highlight whatever Resolume reports as connected. Manual triggers in Resolume now sync the UI without requiring a button press in XOSC. (src/features/inspector/resolumePlaylist.ts,src/features/inspector/ResolumeOutInspector.tsx,src/features/mapping/ResolumeOutNode.tsx) - *Easy-Next router fires the item after whatever Resolume is currently playing, not the next item after our stored pointer.
resolveEasyNextFirereadsconnectedacross the resolved playlist; if any item is connected, the press fires(connectedIdx + 1) % total. Falls back tocurrentIndexonly when nothing is connected (Resolume just opened, blackout, etc.) so first-press behaviour stays predictable. The persistedcurrentIndexsemantics changed from "next to fire" to "currently playing" — the index advances after a successful fire instead of pointing at the upcoming target. (src/features/mapping/useMappingRouter.ts) - Column mode now shows names + thumbnails. Resolume's REST API has no native column thumbnail endpoint — column tiles carry only name + connected status. Built a representative-clip lookup: for each column slot, walk every layer's clip at that index and synthesize a
thumbnailPathagainst the first populated clip found (/api/v1/composition/layers/{repLayerIndex}/clips/{slot}/thumbnail). The same clip's name fills in when the column itself has no user-set name (Resolume defaults likeColumn 1are unhelpful). Resolume's#placeholder in column names (their own rest-example convention for "insert column number here") is now substituted with the 1-based slot. Both the inspector preview list and the node-body thumbnail work for columns now. (src/features/inspector/resolumePlaylist.ts) - Inspector counter relabeled "now playing" with both the connected item's name and the next-up item's name shown after an arrow. The "Will fire on next press →" path stays in sync with the router so the displayed REST URL is the one that actually fires. PreviewRow gets a new dashed-border state to mark the next-to-fire row distinctly from the connected (highlighted) row. (
src/features/inspector/ResolumeOutInspector.tsx,src/features/inspector/resolume-easy-next.css)
Still on the diagnostic harness
- This release keeps
DEBUG_BUILD=1(development React, no minify, DevTools auto-opens) because the underlying production-build hang from v0.7.7/v0.7.8 hasn't been root-caused yet — only the symptom was masked by dev React. The Easy-Next fixes shipped above are renderer-side and orthogonal to whatever's looping in production. Next release will turn DEBUG_BUILD off once the loop is identified from this build's DevTools console (look for "Maximum update depth exceeded" or "The result of getSnapshot should be cached" warnings while interacting with the gloss slider, theme switcher, or any inspector that usesuseSyncExternalStore-backed selectors).
v0.7.10 — 2026-05-01 (diagnostic build)
Diagnostic harness re-armed (re-applies v0.7.6 + v0.7.7 on top of the v0.7.5 known-good base)
- This is not a normal release — it is a one-shot debug installer to find the boot hang. Source matches
v0.7.7(which means it carries both the v0.7.6 title-bar / native-menu / gloss work and the v0.7.7 Easy-Next + help-modal feature). Build is non-minified, ships the development React bundle, and auto-opens DevTools on launch (the v0.6.5 harness, gated byDEBUG_BUILD=1inelectron.vite.config.tsand now set in the release workflow). When this version stalls on the splash, DevTools opens with a real React error name + component stack instead of a blank screen, identifying the offending hook/selector or main-process IPC. Once we know which change in v0.7.6 / v0.7.7 broke boot, the next release reverts to production mode and re-applies just the fixed pieces. Installer is ~3× heavier than a production build; this tag exists for one screenshot and gets superseded byv0.7.11once the cause is known.
v0.7.7 — 2026-04-30
Added
- Resolume Out → "Easy Next Video" playlist mode. New optional
easyNextblock on the Resolume-out node spec: when enabled, every press walks through a detected list of clips (or columns) and wraps1 → 2 → 3 → 1regardless of the user's Resolume layout. Three modes —auto(decides between columns and a single layer based on the live composition: a single populated layer wins, ≥ 2 layers sharing populated slots flips to column-scenes),columns, andlayer-clips(with a layer dropdown). Discovery is the existing REST/WS bridge; XOSC fetches/api/v1/composition, classifies each clip slot as populated/empty (name +video.source+transport.durationheuristics), and resolves the playlist on every press so adding/removing clips in Resolume is reflected without restarting. Index is persisted on the node so the playlist survives reloads. The mapping router intercepts the fire before the configured action path, sends a literaltriggerat/composition/columns/N/connector/composition/layers/L/clips/N/connect, and falls through to the existing path if the composition cache isn't loaded yet (with a one-shotcomposition.refresh()so the next press has data). Continuous values (faders/axes) and release edges still hit the configuredparameter, so an Easy-Next node remains backwards compatible if anyone wires a CC to it. (src/store/store.ts,src/features/inspector/resolumePlaylist.ts,src/features/mapping/useMappingRouter.ts,src/features/inputs/useResolumeComposition.ts) - Inspector "Easy Next Video" panel — collapsible section in the Resolume-out inspector with: enable toggle, mode dropdown, layer dropdown when relevant, "skip empty slots" toggle, recommended-mode hint when
autois picked, current-index counter (2/3 next up) with a Reset button, full thumbnail-preview list (click any item to jump the index there), the exact/composition/...path that will fire next, and a "Refresh from Resolume" button. Picking a layer or flippingskipEmptyresets the index so the user can't land out of bounds. (src/features/inspector/ResolumeOutInspector.tsx,src/features/inspector/resolume-easy-next.css) - Resolume API help modal.
?button next to the Resolume Out inspector header (and a "How do I enable this?" button next to Reconnect) opens a themable, icon-decorated walkthrough for enabling Resolume's Webserver / REST API: open Preferences (withCtrl ,/⌘ ,keyboard hint), pick the Webserver tab, flip "Enable Webserver & REST API" on, verify in a browser athttp://host:port/api/v1/composition, point the XOSC node at the same host:port. Footer has a one-click "Resolume docs" button that goes through the existingapp:open-externalIPC (allow-listedhttp(s)://). All surfaces use theme tokens — works in every shipped theme. (src/features/inspector/ResolumeHelpModal.tsx,src/features/inspector/resolume-help-modal.css) - Resolume-out node body shows Easy-Next state. When Easy-Next is on, the static parameter pill is replaced by an
EASY NEXT • idx/totalpill plus a thumbnail of the clip about to fire (when the resolved playlist is layer-based — column mode skips the thumbnail because Resolume's thumbnail endpoint is per-clip, and arbitrarily picking one layer's clip to represent the column would be misleading). (src/features/mapping/ResolumeOutNode.tsx)
v0.7.6 — 2026-04-30
Changed
- Pads view now hides the right-side inspector strip too. The inspector strip lives in the graph view (it's where you configure node properties) and was redundant in the pad view, where pads have their own per-pad config in the pad editor modal. Single-column grid in
data-view="pads"for bothclosedandopeninspector states. - Native title-bar + File/Edit/View menu replaced by themable chrome. The Windows title bar (where min/max/close lives) and the native menu strip ignored every theme — the user reported them as untheme-able white bands across the top of the window. Switched the main BrowserWindow to
titleBarStyle: "hidden"withtitleBarOverlayso we keep the standard Windows window-control buttons but their background + symbol colour come from the theme; ThemeProvider repaints them via a newapp:set-title-bar-colorsIPC on every theme change. DroppedMenu.setApplicationMenu(null)so the native File/Edit/View/Window/Help row goes away. The Help submenu's contents (XOSC website / Documentation / Changelog / Report a bug on GitHub) are now a small dropdown under a?icon in the topbar action cluster, going through a newapp:open-externalIPC that allow-listshttp(s)://only. Edit-menu accelerators (Cmd/Ctrl-C / V / X / Z / Y) still work via Chromium defaults. - Gloss intensity slider now actually does something.
--glosswas set on:rootby ThemeProvider but no CSS rule consumed it — the slider in Settings did nothing. Wired it into three surfaces: the topbar, every.gnode, and modals with the.glossyclass. Each gets an inset top-edge highlight + a faint top-to-bottom sheen scaled byvar(--gloss, 0). At 0 the surfaces are flat (matches the previous look); at 1 there's a subtle bevel; at 1.5 (the slider's max) it's a clearly glassy chrome.
v0.7.5 — 2026-04-30
Added
- Minimap now draws connection wires between nodes. Each edge renders as a thin line in the minimap, with a userSpaceOnUse linear gradient that fades from the source node's colour to the target's — same colour language as the full canvas. Wires sit under the node dots and are drawn with
pointerEvents="none"so they can't swallow the click-to-pan gesture. - Minimap dots are half-size. The rect for each node now renders at 50 % of the measured node size, centered on the node, so a freshly added node looks like a comfortable dot instead of dominating the panel. The bounding-box / click-to-pan / wire-endpoint maths all stay in canvas-space — only the visual rect is scaled.
Fixed (memory leaks — long-session footprint)
- Serial-out ports no longer leak when nodes are deleted.
useSerialOutputspreviously calledserial.openfor every desired path but neverserial.closefor paths that dropped out of the desired set; over an editing session this stranded SerialPort instances + theirdatalisteners in the main process forever. Track opened paths in a ref, diff against desired, close the difference, and close everything on hook unmount. (src/features/inputs/useSerialOutputs.ts) - MIDI
onmidimessagehandlers are detached on unmount.useGlobalCaptureSinkonly clearedaccess.onstatechangeon cleanup; every wired input kept its handler closure live, pinning the previous React tree across renderer reloads. Track wired inputs in a Set and nullonmidimessagefor each on cleanup. (src/features/inputs/useGlobalCaptureSink.ts) - Resolume thumbnail cache is now LRU-capped at 64 entries. Each thumbnail is a 50–200 KB base64 data URL; browsing a large composition over a long session previously grew the cache without bound. Evict the oldest entries that nobody is currently subscribed to whenever we exceed the cap. (
src/features/inputs/useResolumeComposition.ts) - Resolume reconnect no longer leaves zombie WebSockets. The
closehandler nulledc.wsbut didn't remove theopen/message/close/errorlisteners from the closing socket; under flaky connectivity each reconnect stacked one orphaned ws kept alive by its still-attached node-ws listeners. Addedws.removeAllListeners()in the close handler. (electron/resolume-bridge.ts) - Throttle-state maps now sweep idle entries. Both
electron/resolume-bridge.tsandelectron/osc-engine.tskeyed throttle state on(host, port, parameter|address); entries lived forever even after the user stopped touching that fader. A 60 s sweep drops idle entries with no pending send. - DMX
errorLogGatenow sweeps entries older than 10 s when the gate exceeds 64 keys. A flapping network producing many unique error strings (EHOSTUNREACH 10.0.0.7,…0.8, …) used to grow this map without bound. (electron/dmx/engine.ts)
Fixed (security)
config:import/config:exportIPC handlers now require a path returned from the file picker. Previously the renderer could pass any string; main calledfs.readFile/fs.writeFiledirectly, giving a compromised renderer (XSS, malicious config telling the user to import) a primitive to read~/.ssh/id_rsaor overwrite the app's own bundle. The picker handlers add the chosen path to a one-shot allow-set; import/export consume one and remove it. Audit-confirmed clean for:dangerouslySetInnerHTML/eval(none),shell.openExternal(only hard-coded literal URLs),child_process.exec(none), CSP (strict). (electron/config-store.ts)
v0.7.4 — 2026-04-30
Changed
- Minimap dots resized + drag-to-pan + scroll-to-zoom restored. Bumped bounding-box padding from 10 % to 40 % (min 200 units) and dropped the 40-unit minimum-size clamp so rects shrink proportionally with the node count. Stroke widths are now derived from
vbWidth / MINIMAP_Wso they're a constant ~1.5 px regardless of zoom. AddedonPointerDown/Move/Uptranslating SVG-local coords → viewBox → canvas → main-viewport translate, plusonWheelfor zoom. Node rects + viewport indicator both havepointerEvents="none"so every event hits the SVG.
v0.7.3 — 2026-04-30
Fixed
- Minimap now actually shows nodes. Three previous attempts (v0.6.7, v0.6.8, v0.7.2) all kept using xyflow's
<MiniMap>, just tweaking node colour / stroke width / passing a customnodeComponent. The user re-confirmed v0.7.2 still shipped an empty minimap, so the diagnosis was wrong. Reading@xyflow/react's source revealed thatNodeComponentWrapperInnerreturnsnullwhennodeHasDimensions(node)is false — i.e. before the ResizeObserver has measured the node. OurtoRfNodes()doesn't setwidth/height/initialWidth, so on first mount and on every node-list rebuild the wrapper short-circuited and our custom node component was never invoked. Replaced<MiniMap>with a customPanel-based component that subscribes to xyflow's internal store viauseStore(selector, equalityFn)and renders plain<rect>elements in an SVG. Every node renders unconditionally, with measured dimensions when available and a 240×80 fallback when not. Inlinestyle.fill(highest CSS specificity) carries the per-node colour; nothing inapp.cssor the xyflow stylesheet can override it.
v0.7.2 — 2026-04-29
Fixed
- PadEditorModal crashed with React #310 ("rendered more hooks than during the previous render") on the pencil-edit click. Two
useMemocalls sat after theif (!draft) return nullearly return, so the hook count shrank from 4 to 2 between mount (pad set) and re-mount (pad null). Moved both above the early return.
Changed
- Left rail now expanded by default with text labels. v0.7.0's icon-only 44 px rail was unreadable for first-time users — there was no way to tell which icon was which without hovering. Now defaults to a 96 px column with text labels under each icon; a collapse chevron at the top drops it back to the original 44 px icon-only mode. State persists in
ui.leftRailCollapsed. - Right inspector hidden by default; auto-opens on selection. Previously the inspector ate 360 px of canvas width even when the user wasn't editing anything. Selecting a node or edge auto-opens it; a chevron in the corner closes it again. When closed, a 28 px handle on the right edge re-opens without losing canvas space. State persists in
ui.inspectorOpen. - DMX-out continuous role can now be cleared. Added a "None — discrete only" sentinel value to
DmxContinuousRole;buildDmxValuesshort-circuits whenrole === "none"so the static-value snapshot ships untouched and continuous inputs are ignored.
v0.7.1 — 2026-04-29
Added
- DMX-out nodes can now trigger a pad/sequence instead of writing a static color. Each DMX-out gets a new
modefield on its spec ("color"|"pad", defaulting to"color"). Switch a node into"pad"mode from the inspector and pick one of the saved pads — when an upstream input fires, the router calls into the existing pad engine (togglePad/restartPad/startSequence+stopBySequenceId) instead of building aFixtureValuespayload, so a single keystroke can fan out into a whole choreographed sequence rather than one-channel-per-input wiring. Trigger mode keeps its meaning in pad mode:pulserestarts on press,toggleflips active/inactive,holdplays only while held. The node body shows aPADtag and the active pad name; the inspector hides the universe / fixture / static-value sections in pad mode and shows a "Toggle pad now" test button instead of the channel-snapshot one. Existing graphs hydrate unchanged becausemodeis optional and missing reads as"color".
v0.7.0 — 2026-04-29
Changed
- Topbar "Add X" buttons collapsed into a vertical left rail. The graph view used to carry ten
Add Input/OSC Out/Resolume Out/Resolume In/OSC In/Transform/Monitor/Serial Out/DMX Out/DMX Inbuttons across the top, which had become the dominant horizontal element and forced the clock/import/debug/settings cluster to the right edge. They now live in a 44px vertical rail on the left side ofapp-main, mirroring how DAWs and node editors (Blender, TouchDesigner) organise add-node palettes. Input is pinned at the top with a separator beneath it as the primary action; the rest are icon-only with title tooltips. The pad view collapses the rail column entirely, so it only shows alongside the graph.
v0.6.8 — 2026-04-29
Fixed
- Settings modal content overflowed the viewport with no way to scroll.
.xmodalhadoverflow: hiddenand nomax-height, so as soon as the inner panel grew taller than the viewport the bottom (DMX transmitters list, etc.) was clipped and unreachable. Cap the modal atcalc(100vh - 32px), lay it out as a flex column, give.xmodal-bodyoverflow-y: autoandmin-height: 0, and pin header/footer withflex-shrink: 0so they stay visible while the body scrolls. - Minimap dots vanished entirely after v0.6.7. The
nodeStrokeWidth: 2 → 8+nodeBorderRadius: 0 → 48bump on<MiniMap>ended up rendering nodes as visually-empty blobs. Walked back tonodeStrokeWidth={3}andnodeBorderRadius={2}— same conservative shape as v0.6.6 with a slightly thicker stroke. The fallback color in the node-color/stroke callbacks was alsovar(--accent), which doesn't resolve in SVGfill/strokesince presentational SVG attributes are evaluated outside the CSS custom-property resolution; switched the fallback to a concrete hex (#8b5cf6) so any node that somehow lacks a stored color still paints.
v0.6.7 — 2026-04-29
Fixed
- Adding a Resolume In node crashed the renderer (React #185).
useResolumeThumbnailpaireduseSyncExternalStorewith agetSnapshotthat returned a freshly-allocated{ fetchedAt: 0, url: "", state: "idle" }object on every call when the cache had no entry yet (or when the suppliedpathwasnull— which is the default case for a freshly-added Resolume In node, because its starting parameter is/composition/layers/1/video/opacity, not a clip path, sothumbnailPathForClipreturns null). React 18 compares snapshots by reference; a new object every call meant the snapshot was always "different" and the renderer entered an unbounded re-render loop the moment the node mounted. Worse, the loop was sticky across relaunches because the broken node was now in the saved graph: every subsequent app start re-mounted it and re-crashed before the user could remove it. Same family of bug as the v0.6.6useGlobalHotkeysfix and the v0.6.2EMPTY_PADSfix — third instance of "snapshot reference must be stable" biting in the same release line. Fix: hoist a single sharedidleThumbEntryconstant at module scope (mirroring howuseResolumeCompositionalready handles itsidleEntry) so the empty-cache return is reference-stable. - Zoom controls were white on every dark theme. The
--xy-controls-button-background-color-defaultwe set insrc/styles/app.css(line 164, mapped tovar(--surface)) was being clobbered by xyflow's own.react-flow { ... --xy-controls-button-background-color-default: #fefefe; }rule because the bundler emitted xyflow's stylesheet afterapp.css. xyflow's dark-mode swap selector (.react-flow.dark) never matches because we theme via CSS custom properties on:rootand adata-modeattribute, not via that class. Fix: hoistimport "@xyflow/react/dist/style.css"insrc/main.tsxto before theimport Appline, so app.css now lands later in the bundle and our overrides actually win cascade order.
Changed
- Minimap nodes are now visible at every zoom level. Increased
nodeStrokeWidthfrom2to8andnodeBorderRadiusfrom0to48on the<MiniMap>. With sparse nodes on a large canvas the previous square 2px outlines vanished into the surface tone; now each node renders as a chunky bright dot that's legible without scrubbing the pane to find content.
v0.6.6 — 2026-04-29
Fixed
- Actually fixed the launch crash (round 2). The v0.6.5 diagnostic build (development React, no minify) gave us the readable error:
Warning: The result of getSnapshot should be cached to avoid an infinite loopfollowed byMaximum update depth exceededfrom React'sforceStoreRerender. The culprit:useGlobalHotkeys(originally added in v0.5.2) useduseStorewith a selector that built a freshEntry[]array on every selector call:const entries = useStore((s) => { const out = []; for (...) out.push(...); return out; }). Zustand v5 with React 18'suseSyncExternalStorecompares snapshots by reference; a new array every call meant the snapshot was always "different" → React's loop detector fired on launch. Wrapping the selector withuseShallowfromzustand/react/shallowmakes the comparison element-wise, so an emptyouton a fresh-install graph compares equal across renders and the loop never starts. v0.5.x apparently tolerated the latency (subscription churn was harmless if hydration happened to settle quickly); v0.6.x's heavier initial state changes consistently tripped React's nested-update threshold. - Production build restored. v0.6.5 was a diagnostic build (~3× installer size) shipping development React + DevTools auto-open. v0.6.6 is back to a normal production build now that we have the fix. Sourcemaps stay on for next-time crash debugging.
v0.6.5 — 2026-04-29 — DEBUG BUILD
This release is intentionally a diagnostic build, not a normal release. v0.6.4 fixed the xyflow snapGrid / inline-callback loop that was visible in the previous traces; another React #185 was reported afterward but with no userspace frames in the trace, only react-dom internals — so it's likely the React useSyncExternalStore "snapshot changed" detection path triggered by another non-stable selector. v0.6.5 ships the development React bundle (readable error messages with component names), turns off renderer minification, and auto-opens DevTools on launch. The installer is roughly 3× the size of v0.6.4. Once we have the actual error message + component path, v0.6.6 will revert to a production build.
Added — diagnostic instrumentation
- Development React bundle swapped in via
define: { "process.env.NODE_ENV": "development" }. Errors will surface asMaximum update depth exceeded. The component <Name> calls setState (...)instead ofMinified React error #185; visit https://reactjs.org/docs/error-decoder.html?invariant=185. - Renderer minification disabled. Stack frames now name actual files + functions.
- DevTools auto-opens on the right side of the main window every launch in this build, baked in at compile time via Vite's
define: { __XOSC_DEBUG_BUILD__: true }for the main bundle.
Fixed — additional unstable selectors
useStore((s) => s.settings.dmx?.userProfiles ?? [])inDmxOutNode,DmxOutInspector,DmxSettingsPanelwas returning a fresh empty array on every selector call when no user profiles existed (the default), causing the sameuseSyncExternalStoresnapshot-changed loop that v0.6.2 fixed forpads/clocks. NewEMPTY_USER_PROFILESsingleton; same pattern asEMPTY_PADS/EMPTY_CLOCKS/EMPTY_TRANSMITTERS.
v0.6.4 — 2026-04-29
Fixed
- Actually fixed the launch crash. Reading sourcemaps for the consistent
index.js:320:23stack frame revealed the line was inside@xyflow/react/dist/esm/index.js— theStoreUpdaterblock that syncs<ReactFlow>props into xyflow's internal Zustand store. xyflow tracks ~50 props (the full list includessnapGrid,onEdgesDelete,onConnect, etc.); when any tracked prop's reference differs from the previous render, the StoreUpdater callsstore.setState({field: newValue}). Our<ReactFlow>was passingsnapGrid={[gridSize, gridSize]}— a brand-new array on every render — and inline closures foronEdgeClick,onPaneClick,onEdgesDelete. Each render produced 4 new references, fired 4 internal setState calls, cascaded through xyflow's many subscribers, and cycled into React's max-update-depth abort. The whole renderer tree unmounted. Three previous patches that "fixed the launch crash" all touched files outside this loop, which is why the sameindex.js:320:23stack persisted acrossv0.6.0→v0.6.1→v0.6.2→v0.6.3with identical behaviour. - Stable references for every tracked ReactFlow prop.
onEdgeClick/onPaneClick/onEdgesDeletenow useuseCallback.snapGridusesuseMemokeyed ongridSize.deleteKeyCode,defaultViewport,proOptions, and the MiniMapnodeColor/nodeStrokeColorcallbacks are now module-level constants. The launch race no longer exists.
v0.6.3 — 2026-04-29
Added — diagnostic build (the launch crash is still happening)
- setState loop detector wired into the Zustand store. v0.6.0 / 0.6.1 / 0.6.2 each thought they fixed the React #185 launch crash and each was wrong; the minified production stack consistently points at
index.js:320:23calling ZustandsetStateduring commit, but with all our code's selectors now stable-ref it's clear the culprit lives elsewhere (most likely vendor —@xyflow/reactor another internal store). v0.6.3 wraps everyset()in the renderer-side store with a counter that resets each ~16 ms frame; once a frame logs 80 mutations, it captures the first 5 stacks and throws an explicit error with all five included. The new<ErrorBoundary>then renders that error visibly. Net effect: the next launch crash will name the action that's looping rather than failing through React's minified decoder.
v0.6.2 — 2026-04-29
Fixed
- v0.6.1's launch crash didn't actually fix v0.6.0's launch crash. With sourcemaps enabled in v0.6.1, the renderer console finally pointed at the real culprit: the defensive
?? []selector pattern I added in v0.6.1 returned a fresh empty array on every Zustand selector call, breaking Object.is equality and causing every component readingsettings.dmx.to re-render on every store change. Combined with the ClockBar BPM writeback effect, that produced the React #185 "max update depth exceeded" loop. - Stable singleton fallbacks. New
EMPTY_PADS/EMPTY_CLOCKS/EMPTY_TRANSMITTERSexported from the store. Selectors fall back to these instead of[], so the result is reference-stable when the underlying slice is missing — Zustand bails on Object.is and skips the re-render. - Removed ClockBar's BPM writeback effect. It tried to keep the persisted BPM in sync with the runtime tap-tempo state via a useEffect that ran on every
clockschange. Even with the v0.6.1 mismatch guard it remained the most plausible loop trigger. Tap onClick already writes BPM directly throughsetClockBpm; future external clock sources (MIDI Clock, OSC/clock/bpm) will use the same explicit-write path. Net behaviour: identical for tap-tempo users; one fewer render-loop trap door.
v0.6.1 — 2026-04-29
Fixed
- Renderer infinite-update loop on launch. The Topbar
ClockBarwidget had a feedback chain (setClocks → emit → subscriber → force re-render → effect re-runs) that, under certain hydration timings, hit React's "max update depth" guard and unmounted the entire React tree — leaving the user staring at the gradient background with no visible UI. The chain has been rewritten to skip emits when nothing changed, separate read-only display refreshes from store-write paths, and avoid setState inside the listeners-iteration scope. - Defensive hydration of
settings.dmx. Older configs (v0.5.x had nodmxblock at all; early v0.6 builds had only{transmitters, userProfiles}) now backfill cleanly with current defaults via a newensureDmx()helper instead of producing a partial shape that crashed the first selector readingdmx.pads/dmx.clocks. All renderer selectors that readsettings.dmx.are now nullish-safe. - Visible crash screen. New
<ErrorBoundary>wraps the app — uncaught render errors now show the message + stack + a hint about renaming the config file, instead of failing silently to a blank background. The crash also pushes asystem-source log entry so the debug log captures it.
Added
- Help menu. Custom application menu replaces Electron's default. Help → XOSC website / Documentation / Changelog open the live site; Report a bug on GitHub opens the issues tracker. Default View / Edit / Window roles preserved.
- Renderer sourcemaps.
electron-vite.config.tsenablessourcemap: truefor the renderer build. Adds ~2 MB to the installer in exchange for crash stacks pointing at real file/line locations instead of minified bundle offsets.
v0.6.0 — 2026-04-29
Added — DMX support (Track A: hardware backends + graph integration)
- DMX engine in main process. New
electron/dmx/module owns one universe buffer per logical universe id and one transmitter per (kind, port). OnesetIntervalper transmitter at the protocol's native tick rate (Art-Net / sACN: 23 ms; Open DMX: 33 ms; Slow-break: 50 ms; Enttec Pro: 100 ms). Per-tick pull fromuniverseBus.snapshot(universeId)so writes always land on the next frame. - Five hardware backends. Single
Transmitterinterface, one file per protocol, all ported from TagTable's production-tested implementations:
Op-Dmx 0x5000 + sequence number byte rolling 1..255.
- sACN / E1.31 — UDP multicast at 239.255.<universe-high>.<universe-low>:5568, full ACN packet (root + framing + DMP layers), CID generated once per process, source name "XOSC".
- Open DMX — FTDI USB serial, 250 kbaud 8N2, BREAK via port.set({ brk: true }) held ~1 ms, then 513-byte data frame.
- Enttec USB DMX Pro — framed protocol at 57600 baud 8N1: [0x7E][0x06][len-LSB][len-MSB][0x00 + 512 B][0xE7].
- Slow-break — CH340 / PL2303 adapters that lack a real BREAK line: switch to 90 kbaud, write 0x00 (10-bit frame stays low ~111 µs > the 88 µs DMX BREAK requirement), switch back to 250 kbaud and write the data frame.
dmx-outnode kind. New sink that resolves a fixture profile + start channel + per-role values into a list of channel writes and ships them to the universe bus. Pulse / toggle / hold trigger modes; continuous inputs override the configuredcontinuousRolewith their normalized 0..1 value (scaled to byte 0..255). Topbar DMX Out button. Node body shows live colour swatch reflecting the most recent write, plus the existing sink-rate gauge.dmx-innode kind. New source that listens to inbound Art-Net frames on(bindHost, port)(UDP socket refcounted across nodes that share an endpoint, same shape asosc-in). Fires onto the input bus when a watched channel's value changes by at least the configured threshold (drops jitter). Supports a contiguous range up to 16 channels — each slot fires its own event with a per-channel signature. Topbar DMX In button.- Fixture profile library. Eight built-ins covering the common cases: Dimmer (1ch), RGB / RGBW / RGBA / RGBAW Par, RGB+Strobe, RGB+Dimmer+Strobe, Moving Head 12ch (pan-coarse + pan-fine + tilt-coarse + tilt-fine + speed + dimmer + strobe + R/G/B/W + function). Schema in
src/features/dmx/fixtures/types.ts; user profiles persist undersettings.dmx.userProfiles(modal editor parked for v0.7 polish — JSON edits via config import/export work today). - DMX settings panel. Lives under Settings → DMX transmitters. Add / remove / enable transmitters, pick serial port (auto-discovered via
dmx:list-serial-ports), set Art-Net target IP + UDP port + universe / sACN universe / serial path. Per-row diagnostics: FPS, drop rate, frames sent, error count, last-error message, health badge (good / warning / critical / no_output). Test pattern button cycles R/G/B/W/off on channels 1-3 of the picked universe. - Logger source
"dmx". Open / close / send-error / drop-rate-warn entries route throughpushLogand surface in the debug panel under a new dmx chip. Errors rate-limited per (transmitter id, message) into 200 ms windows so a dead network doesn't flood the buffer. - Cycle detection over DMX. A
dmx-outwriting universe U channel C virtually feeds anydmx-inreading the same channel —cycleDetectsynthesises the virtual edge so the wire renders amber, the same way Resolume parameter-equality loops are flagged today.
Added — DMX support (Track B: sequences + pads + mixer)
- Pad launcher (Sequences view). New top-level view alongside the node graph (toggle in the topbar between Graph and Pads). A configurable grid of pads, each owning a sequence (cue list), color, hotkey, mixer slot/priority/mix-mode, and optional shared-clock binding. Eight starter sequences seed on first run via Load defaults (Warm Wash, Color Cycle, Slow Fade Blue, Strobe Test, Police, Gentle Lounge, Bass Hit, Blackout) with QWERTYUI hotkeys. Click a pad to toggle, Shift+click to restart, Spacebar to blackout (matches TagTable).
- Sequence engine. Renderer-side rAF master tick advances every active layer's playhead, interpolates per-role fades, and hands composed values off to the mixer once per frame. Looping sequences wrap their cue index on each cycle. Layers can run concurrently for the same or different sequences.
- Mixer with slot exclusivity + mix modes. Replaces Track A's last-write-wins placeholder. Per channel: candidates within a slot resolve to the highest-priority layer; across slots, layers compose by max (default — brightest wins, ideal for additive RGB lighting), add (sum, clamped 255), or replace (last write wins). Graph
dmx-outwrites always overlay on top so a manual fire wins over an ambient pad. Composed 512-byte frames push to main viadmx:set-frame. - Cue-list editor. Edit a pad's name, color, hotkey, target fixture (universe + profile + start channel), playback (duration + loop + clock), mixer hints (slot + priority + mix mode), and cue list (per-row time, fade, all role values) inside one modal. Per-cue
emitfield lets a cue fire a synthetic input event into the bus when it lands — wire a cue's signature to a Resolume out node and the bass-drop fires both the chase and the OSC trigger at the right beat. - Effect generator. Optional cue filler in the editor: pulse / chase (R-G-B) / color cycle (even hues) / fade rainbow, parameterised by step count and fade smoothness. Replaces the cue list with the generated pattern.
- Shared clocks + tap tempo. New Clock bar in the topbar shows each clock's BPM with a tap button + reset. Layers bound to a clock derive their playhead from the clock's origin so sequences started 100 ms apart still stay in phase. Tap-tempo computes BPM from a rolling 4-tap average; values outside [30, 300] are rejected. The default
mainclock seeds with no BPM until you tap. - Pad ↔ input-bus integration. Pad hotkeys route through the existing keyboard input layer (
pad:<padId>:firesignatures), so any input source — keyboard, MIDI, OSC, gamepad, Resolume parameter via threshold transform — can fire a pad. Conversely, a pad firing emits itspad:<id>:fireevent so a graph node can wire to "Warm Wash starts" and trigger Resolume in lockstep. Cueemit.signaturedoes the same per-cue. - Per-pad live state. Active pads show a thin progress bar fed from the engine's playhead; a glow + accent border indicates the pad whose layer is the live one in its slot.
Notes
- Beat-locked sequence playback (sequences re-derive duration from the clock's BPM) is parked for v0.6.1 polish — tap tempo gives you a number you display + visualise; cue timings stay in seconds.
- External clock sync (MIDI Clock 24 PPQN, OSC
/clock/bpm) is also v0.6.1.
v0.5.4 — 2026-04-29
Added
- Serial output node. New sink kind that writes to a COM port, completing the Resolume → Arduino LED feedback round-trip. Pick a port (auto-discovered from
serialport.list()), baud rate, and a payload template with{v}(0..1 float, 3 decimals),{V}(0..255 byte), and{s}(state) placeholders. Line ending is configurable (LF / CRLF / CR / none). Discrete inputs honour the trigger mode (pulse / toggle / hold); continuous inputs stream their normalized value through the placeholders. Sharing the port with a serial-input node is supported — the bridge opens it once and routes both directions. NewSerial Outtopbar button + inspector with live test send. - Wires breathe on traffic. Every edge dispatch fires a 280ms halo pulse along the wire — a single press shows a single bright pulse; a continuous axis at 60 Hz keeps the wire continuously lit. Pulse colour is the source node's colour. Idle wires render unchanged. New
edgeFireBuskeeps the signal scoped so off-canvas edges don't pay the cost. - Serial input parser controls. The serial bind UI now exposes a Raw bytes toggle (chunks become triggers without line-splitting) and a Delimiter field for line mode (paste
\n,\r\n,\r, or\t— XOSC decodes the escape when opening the port). Previouslyparserwas API-only.
Added
- Custom theme editor. Settings now ships an in-app editor below the built-in theme grid: pick six seed colors (background, surface, accent, accent-2, text, border) with a clickable swatch (OS color picker) and a hex field. Surface 2/3, border-strong, text-dim/mute, and accent glows are derived automatically via chroma-js so the result stays internally consistent. Light/dark mode auto-detects from the background luminance. Every change live-applies; Reset clears the custom theme and returns to whichever built-in is active. Seeds persist with
customThemeinxosc.config.jsonso the editor reopens preloaded. - mDNS auto-discovery for hosts. Resolume Out / Resolume In / OSC Out inspectors get a host picker dropdown of LAN-discovered peers — no more hunting for IP addresses. The main process runs
bonjour-servicebrowsers for_resolume._tcp,_osc._udp, and_companion._tcp; each picker is filtered by service kind so the Resolume inspector doesn't show Companion controllers. Selecting a peer fills both host and port. mDNS is broadcast-only on224.0.0.251:5353— XOSC does not advertise itself. On a hardened venue with multicast blocked, the picker stays empty and manual entry still works as before.
v0.5.2 — 2026-04-29
Added
- Cycle-warning UI. Edges that participate in a logical feedback loop now render with an amber stroke and a
⟳ looppill at the midpoint. Detection includes static graph cycles AND virtual loops that close through Resolume parameter equality (aresolume-outwriting parameterPis treated as feeding anyresolume-inreadingPon the same host:port). Non-blocking — the depth cap and echo guard still terminate the loop at runtime; this is a static design-time hint so you can spot the wiring at a glance. - Per-sink event-rate gauge. OSC-out and Resolume-out nodes show a tiny
events/secpill while traffic is live. The pill turns amber above 60 events/s, red above 200 — useful for spotting an over-wired continuous source from across the canvas. Side-channel ring buffer (out of Zustand) so 60 Hz axis traffic doesn't trigger React re-renders across the app. - Resolume swagger autocomplete. The Resolume parameter picker now fetches
/api/v1/docs/swagger.jsonalongside the composition tree and unions paths in. Effect / dynamic params that don't appear in the composition (because they haven't been added yet) are still pickable as long as Resolume's swagger lists them. - Global keyboard hotkeys. New per-input toggle in the keyboard input inspector. When on, XOSC registers an Electron
globalShortcutfor that key so it's captured even when the window isn't focused. globalShortcut fires once per press; XOSC synthesizes a release ~120 ms later so toggle / hold downstream stays predictable. Modifier combinations and most non-symbol keys are supported; the failure-to-register reason is logged.
v0.5.1 — 2026-04-29
Added
- Per-edge throttle override.
MappingEdge.throttleMs?overrides the engine's default 25 ms continuous-burst throttle on a per-wire basis. Edge inspector exposes a Toggle + Slider — useful when one source feeds many sinks and only some need calming. - Save debug log to file. DebugPanel gets a Save button alongside Copy / Clear. Writes the filtered buffer as JSONL via a new
log:saveIPC +dialog.showSaveDialog. Default filename isxosc-log-<timestamp>.jsonl. - OSC Monitor node. New sink-only node kind that visualises traffic on its left handle: live-scaling sparkline of the last 120 numeric values + the last 6 raw timestamped samples. Doesn't fire any side effects; great for stage debugging without opening the debug panel. Topbar Monitor button.
Fixed
- Toggle state survives rebind. When a sink's incoming edges or
triggerModechange, its per-sinktoggleStateentry is now cleared so the next press starts from a known state instead of mid-flip. Stale entries for deleted sinks are also dropped.
v0.5.0 — 2026-04-29
Added
- Loop protection: dispatch lineage + depth cap.
InputFiredEventnow carries an optionalcausedBy: { nodeId, depth }. The mapping router drops any event whosedepth > 8and emits a warn-level log entry. Foundation for safe multi-hop chains. - Resolume echo-suppression on
resolume-in. When aresolume-outwrites parameterP, the dispatch tracker remembers the write for ~200 ms. The nextparameter_updateforPis dropped (default) — kills theresolume-in → resolume-out → Resolume → resolume-inloop without throttling user-driven motion. New per-node "Echo guard" toggle in the resolume-in inspector lets the round-trip through when you actually want it; depth cap still terminates at 8 hops. - Inbound OSC listener (
osc-innode). New source node kind that opens a UDP listener on a configurable(bindHost, port)and emits matching packets onto the input bus, ready to be wired to any sink. Address patterns support(any non-slash chars) and?(one char). Single listener per(bindHost, port), refcounted across nodes — many nodes can listen on different addresses on the same port. Default bind is127.0.0.1(loopback only);0.0.0.0is opt-in with a clear "LAN visible" warning in the inspector. Topbar button + node body shows live-hit indicator and last numeric value. - Math / logic
transformnodes. New mid-graph node kind that's both a sink and a source. Initial set: clamp, scale (range remap), invert, curve (gamma), hold-timer (separatetap/holdoutputs), double-tap, threshold (hysteresis withhigh/lowoutputs), latch (toggle on press), edge (rise / fall / both). Stateful ops keep state per(transformNodeId, sourceId)and reset on app reload. Multi-output ops expose multiple right-side handles; edges remembersourceHandle/targetHandle. - Dispatch mode per edge. Edges now carry an optional
dispatch: { mode, order, delayMs?, requirePrevOk? }. Modes: parallel (default; current behaviour), sequential (await each previous edge before firing), after-prev (sequential + skip on previous error). Edges from the same source group sort byorderascending. New EdgeInspector exposes the controls; click an edge to select it. ColorMixEdge renders dashed strokes + an order/mode badge for non-default edges.
Changed
- Mapping router rewrite. Multi-hop traversal: walks
source → [transform …] → sinkwithMAX_DISPATCH_DEPTH = 8bounding cycles. Threads dispatch depth through to sinks;resolume-outsetactions record their write into the lineage tracker so a Resolume rebroadcast can be attributed back to the originating sink. Per-edge dispatch mode honored. - Edges now carry optional
sourceHandle/targetHandle(transform multi-output) anddispatch(per-edge ordering); paste preserves both.
v0.4.0 — 2026-04-29
v0.4.0 — 2026-04-29
Added
- Resolume composition picker. Resolume Out and Resolume In nodes now ship a live picker that fetches
GET /api/v1/composition, walks the tree (layers / clips / columns / decks / video / audio / effects / params), and surfaces every leaf as a searchable dropdown — paths, value types, ranges, and current values. The picker on Resolume Out narrows toconnect/trigger entries whenAction = trigger, and to value-bearing parameters otherwise. Manual path entry still works for by-id paths or anything we don't recognise. - Live clip thumbnails. When the selected parameter is under a clip, XOSC pulls
GET /api/v1/composition/layers/{L}/clips/{C}/thumbnailand shows the preview inside the picker row, in the inspector, and on the node body. The on-node preview polls at ~1.5 s while the WS bridge is open, stops when it disconnects. - Canvas keyboard shortcuts.
Delete/Backspaceremoves selected nodes (and any wires touching them).Ctrl/Cmd+C/X/Vcopies, cuts, and pastes selected nodes. Paste generates fresh IDs and offsets each successive paste by 24 px so they fan out instead of stacking. Wires travel with the paste only when both endpoints are part of the selection.Shift+click extends the canvas selection. - Theme picker grid. Settings now shows all 10 built-in themes as a 2-column grid of preview cards (bg + accent + accent-2 + surface + border swatch), instead of the old single-line dropdown.
- All-users installer option. The NSIS installer now offers per-user vs all-users with a UAC prompt (
allowElevation: true), so the app can land inProgram Filesfor shared workstations. - New IPC:
resolume:fetch-jsonandresolume:fetch-thumbnailin the main process. The renderer never touches HTTP directly — main proxies it from a tinynode:httphelper that times out at 4s.
Changed
- App / installer icon regenerated from
src/assets/xosc-mark.webp(skull-pinup mark) instead of the placeholder XOSC-text-on-purple-radial —build/icon.{png,ico,svg}andbuild/icon-256.pngrebuilt as a multi-size.ico(16/32/48/64/128/256). Newscripts/build-icons.shregenerates them deterministically. - Topbar buttons rebalanced:
+ Inputis now the primary button (it's the first action a new user takes); OSC Out, Resolume Out, and Resolume In are subtle. - Topbar logo light-mode override fixed — the selector previously keyed off
data-theme(never set); now correctly usesdata-mode="light"for Bone and Newsprint themes. - MiniMap now sets
bgColor="var(--surface)"and uses thicker node strokes so layouts read clearly at small zoom on dark themes. - Resolume docs updated to reflect what XOSC actually ships now (vs the old "v0.2 will add" copy). New pages:
Resolume — parameter picker & live previewsandKeyboard shortcuts.
v0.3.0 — 2026-04-29
Added
- Resolume input (subscribe) node. New
resolume-ingraph node subscribes to a Resolume parameter via the WS bridge and acts as an input source. Wire it into an OSC out or Resolume out to mirror or rebroadcast a parameter — the same mapping logic that handles keyboard / MIDI / gamepad applies. Live status pill on the node, inline display of the most recent value, Reconnect button in the inspector. New "Resolume In" button on the topbar. - Per-input range / curve mapping. Continuous inputs (MIDI CC, pitch bend, gamepad axis, Resolume feedback) gained a value-transform editor in the inspector: invert, min / max range, curve (linear / log / expo / expo² presets + free slider 0.1..4), center deadzone, low / high deadzone. Identity transforms aren't persisted, so saved configs stay clean.
- Source / sink helpers in store.
isSourceNode/isSinkNode/applyTransform/resolumeInSignatureexported so feature code can route uniformly across input + resolume-in sources. - Mapping router refactored to per-edge dispatch. Each (source, sink) edge now applies that source's transform independently — multiple inputs matching the same physical event can each fire downstream sinks with their own remapped value.
Fixed
- CI: typecheck-web added
@types/nodetoweb/package.jsonsoastro checkresolvesnode:fs/promises/node:path/process.cwd()references in build-time loaders. Was passing locally because of a hoisted dep at the repo root; failed in CI because the verify job only installsweb/'s deps. - CI: deploy-site / release workflows no longer fail at workflow-validation time when
SITE_DEPLOY_SSH_KEYisn't configured. The optional rsync / scp upload is gated behind a step output instead of${{ env. }}inif.
v0.2.0 — 2026-04-29
Added
- OSC throttle for continuous bursts.
electron/osc-engine.tscoalesces continuous values perhost:port:address(leading + trailing flush, default 25 ms). Discrete pulses still pass through synchronously.OscSendRequestgainedcontinuous?: boolean+throttleMs?: number. - Continuous values from MIDI CC, pitch bend, and gamepad axes. Descriptors carry
continuous: true;inputBusevents carry a normalizedvalue: number∈ [0,1]. Gamepad axis polling joined the existing button polling loop. - Mapping router routes continuous values to the first numeric arg of every downstream sink (
farg gets the float verbatim;iarg getsround(v127)). - Resolume modifier-prefix preset in OutputInspector: prepends
"a"/"+"/"-"/""/"?"as the first string arg for Resolume-style relative parameter sets. - Resolume WebSocket output node. New
resolume-outgraph node speaks the Resolume WS API (action:trigger,set,subscribe,unsubscribe). One socket perhost:port, exponential-backoff reconnect (cap 30 s), bounded send queue while connecting, per-parameter throttling forset, live status pill on the node, and a Reconnect button in the inspector. - Topbar "Resolume" button spawns the new node at viewport center.
- Real logo + favicon set. Brand SVG retired in favor of the actual XOSC mark;
filter: invert(1)for dark themes; light themes (bone, newsprint) keep it black. Site header, footer, splash, Electron Topbar, and full favicon/manifest set all share one webp.
Changed
- CSP locked down for offline / air-gapped venues. No remote font/connect/object/frame loads in
index.htmlorsplash.html. Renderer never makes outbound HTTP at runtime — only local WS to the Resolume host the user picks (default 127.0.0.1). - External navigation blocked.
setWindowOpenHandlerdenies popups;will-navigaterejects any non-file/non-dev URL. - Font fallback chain prefers Windows 11 system fonts (
Segoe UI Variable Display,Cascadia Mono) so the UI renders correctly on a venue laptop without the design fonts installed. - Debug log polish. Continuous OSC + WS sends log at
debuglevel (info filter shows discrete fires only). Resolume bridge logs every send / receive / queue / state-change main-side; renderer pipesresolume:onMessage+resolume:onStatusinto the same buffer. DebugPanel gained source filter, level filter, free-text search acrossdatapayloads, tail toggle, and per-row inline data summary.
Fixed
- Gamepad axis prevAxis bug — first deliberate move post-bind now reliably trips the delta gate.
- Hydration robustness —
hydrateFromSnapshotfilters unknown node kinds and drops dangling edges whose endpoints don't exist (forward-compat for older saves).
Audit
- Confirmed no
autoUpdater, Sentry, Bugsnag, orcrashReportercalls anywhere.electron-builder --publish neverkeeps the build pipeline offline-only.
v0.1.0 — 2026-04-26
- Initial XOSC release. Electron app + Astro site + CI.