Uh oh!
There was an error while loading. Please reload this page.
Refactor the plugin to use Elgato's node SDK. - #17
Merged
Conversation
zv1n
commented
Aug 13, 2026
Contributor
- Complete refactor to new SDK.
- Migrates any settings changes fro older versions.
- Fixes Meld Studio reconnect idempotency.
- Swaps icon state to match the third party icon state mapping.
After a WebSocket reconnect $MS.meld is replaced by a fresh QWebChannel
object, so registerTrackObserver (and any other per-channel wiring) was
lost. Outbound RPCs kept working, but inbound gainUpdated signals
stopped, leaving dials showing stale names/values. The base class also
leaked a $MS.on('ready') listener on every didReceiveSettings delivery,
so over time reconnects replayed an ever-growing stack of stale
callbacks.
- Move the 'ready' reinit listener to a single per-instance handler in
MeldStudioPlugin that iterates the current contexts with current
settings, instead of accumulating one per settings delivery.
- Clean up this.contexts on willDisappear and guard didReceiveSettings
so phantom contexts aren't re-registered on reconnect.
- Default onReady is now a no-op; sessionChanged is already re-emitted
right after ready, so the old session-wide call was redundant.
- VolumeStepper: drop its own didReceiveSettings override (base handles
it), hoist the maybeUnregister willDisappear handler, align onReady
with the base signature, and reset trackInfo to defaults when a track
is missing from the session so stale names can't persist.connectGain was wiping trackInfo on every reconnect, causing the dial to flash 0%/-60dB and unmuted before the next gainUpdated arrived. Only seed defaults when no prior state exists.
This should prevent streamdeck controls from getting out of sync with Meld.
Replace the legacy SDKv2 browser-hosted architecture with Elgato's official @elgato/streamdeck Node SDK, running the whole plugin as a single long-lived Node process. This structurally fixes the chronic state/reconnect/hydration/dial-desync bug class: - Single QWebChannel connection to Meld shared by all actions and PIs, with a centralized reconnect + track-observer registry (src/meld/). - One authoritative session snapshot (MeldSession) as the source of truth. - Declarative MeldAction.render() lifecycle replacing the race-prone willAppear/getSettings/hydrateAll chain. - Optimistic-value-with-TTL reconciliation for dials/toggles; the authoritative state always wins (the dial-desync fix). - Property inspectors are thin and hold no Meld connection; they request session data from the plugin via ui-bridge.ts. All 14 action UUIDs and visual assets are preserved for seamless upgrade. Manifest moves to Node 24 runtime, SDKVersion 3, MinimumVersion 7.1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four adversarially-verified review rounds over the Node SDK migration, fixing every confirmed finding: State correctness: - Add DisableAutomaticStates to all two-state actions and invalidate the state/image dedupe caches on key press, so key icons can never stick wrong after presses, forced toggles, or disconnected presses. - Volume dial: echo-safe optimistic reconciliation (epsilon guard so echoes of older setGain calls can't revert newer values), session- snapshot gain fallback, no-op instead of slamming volume when gain is unknown, observer cache cleared on disconnect / track unbind, mute from the fast gainUpdated path wins over the slower snapshot, and optimistic expiry repaints authoritative state. - Transition dial: optimistic staged-scene selection that survives echoes, id-keyed rotation anchor that survives scene reorders and transitions, and blip immunity across every render entry point. - Show Scene: unconfirmed presses self-correct instead of staying lit. - Record/stream keys hold their last state through connection blips. - Toggle layer/effect validate the scene->layer->effect parent chain before rendering state or sending commands. Process robustness: - Contain exceptions in QWebChannel message dispatch, response handling, emitter listeners, per-action renders, and Stream Deck sends; one bad frame or throwing subscriber can no longer kill the plugin process. - A handshake failure closes the socket into the backoff reconnect path instead of leaving a silent zombie connection; backoff only resets after a fully completed handshake. - Builds without the track-observer feature (gainUpdated signal or register/unregisterTrackObserver) degrade to snapshot-only gain. PI and packaging: - Parent dropdown changes clear downstream selections atomically; stale chains render "[No Selection]" with empty child lists. - Remove Nodejs.Debug from the production manifest. - install.sh purges stale legacy files (--delete) and restarts the plugin so deploys take effect. Tests: new suite covering OptimisticStore expiry semantics, TypedEmitter listener isolation, and dB/gain math; TypedEmitter decoupled from the SDK so tests stay pure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds tools/fake-meld: a stand-in for Meld Studio's WebChannel server that the plugin cannot tell apart from the real app, plus preloadable scenarios. `npm run fake-meld` serves one with Meld closed, and keyboard controls switch projects, drop the connection, or flip streaming and recording — states that are slow or awkward to stage by hand. The server mirrors Meld/Utils/StreamDeckController.qml (API version 2, all twenty methods, the gainUpdated signal, and the behaviour behind them) and the snapshot shapes in Meld/StreamDeckMapper.cpp. The test suite now drives the same server, so the tool and the tests cannot drift. Reading the Meld source corrected two assumptions and exposed two bugs: - A track in the session snapshot carries no `gain`; gain arrives only via gainUpdated, which registerTrack emits immediately on registration. Replay's volume controls read the (always absent) snapshot gain and so stepped from an assumed 0, slamming the replay clip to near-silence instead of nudging it — a bug inherited from the legacy plugin. Replay now observes the track like the volume dial does, and refuses to step until a gain is known. The equivalent dead fallbacks in volume-stepper are gone. - Scenes carry `vertical` (property inspectors filter on it), layers carry `isEffectivelyVisible` and `text`, and `setClientName` exists. The types now match the source of truth. Connection fixes: - dispose() (and any teardown that detaches the socket handlers) left `ready` true and `meld` set, describing a transport that was gone. A later connect() would then find a stale ready flag, which also disarmed the handshake watchdog and could wedge the plugin against a silent peer forever. Teardown now records the disconnect. - The watchdog's `|| this.#ready` guard could only ever suppress a fire that was needed, never a spurious one; the generation check already covers that. Removed. - The handshake timeout and reconnect delay are injectable, so the watchdog is exercised in tests rather than waited out. Tests: the connection suite now covers instance replacement, observer re-registration, immediate gain on registration, resource-leak-free churn, and recovery from both a stalled peer and a wrong peer — the last two asserting a redial actually reaches the bad peer, since asserting "not ready" alone passed even with the watchdog removed. Verified by mutation: disabling the watchdog or the teardown fix fails these tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The previous commit shipped `meld.sessionChanged.connect(() => {})` — an
empty callback left behind by a review agent's mutation experiment. The
line was correct in de0f0ac and b658958 and silently neutered in f56af3e;
the audit that followed grepped for mutation markers, which an empty
arrow function does not match.
The effect was severe and quiet: the plugin connected, hydrated once, and
then froze. Nothing driven by `sessionChanged` — scene switches, layer
and effect toggles, mute and cue state, project loads — ever repainted
again until a reconnect. Streaming and recording kept working, which is
what made it look healthy.
No test caught it because nothing exercised a live session update: the
connection suite only asserted on the connect-time snapshot, and no test
drove an action's render lifecycle at all.
Adds the coverage that closes that gap:
- test/connection.test.ts asserts a session change made in Meld reaches
listeners and updates the snapshot, not merely that hydration worked.
- test/session-toggle.test.ts drives real ToggleLayer/ToggleEffect/
ToggleMute actions end-to-end against the fake Meld: a key follows the
session after a press, a legacy profile's stale scene/layer chain still
toggles the intended item with the real parent chain derived from the
session, and a press for an item this instance lacks neither sends nor
paints.
- test/volume-stepper.test.ts covers the dial reconciliation rules,
including the desync scenario itself — the fake Meld can now delay its
`gainUpdated` echo (echoDelayMs), so an update for an older write
arrives while a newer one is displayed.
- test/session.test.ts covers the MeldSession selectors.
Both regressions are mutation-verified: reintroducing the empty callback
fails two suites, and removing the gain echo guard fails the desync test.
Also fixes a defect the selector tests found: `has()` used `in` and
`get()` indexed directly, so ids like "constructor" or "toString"
resolved through Object.prototype and conjured items that do not exist.
Lookups are own-property checks now.
Supporting changes: MeldAction takes an optional connection so tests can
point actions at a fake Meld (production still uses the singleton), and
the test loader compiles TypeScript rather than relying on Node's type
stripping, which cannot parse the SDK's decorators.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>An expert panel over the action and property-inspector layer independently confirmed the sessionChanged regression fixed in 05e6284 and found four more real defects. Plugin fixes: - base.ts: setKeyState/setKeyImage read the dedupe cache before the send and wrote it after, so two updates arriving in one event-loop turn could drop the newer state — the key latched on the older value with a cache that agreed, and nothing repainted it. The cache slot is now claimed before the await and rolled back only if the send fails and no newer value has claimed it since. - replay.ts: volume up/down stepped from the gain captured by the last render. `gainUpdated` refreshes the cache without re-rendering, so a fader moved in Meld since then was stepped from a stale base. The press now reads the current value. - replay.ts: the shared "replay-gain" observer was never released, so it was re-registered on every reconnect for the lifetime of the process even with no replay key on screen. It is dropped when the last one goes. Fake Meld fidelity (a fake that is wrong in the same direction as the plugin hides the bug): - setStagedScene now ignores vertical scenes, matching the QML, which resolves it through Session.scenes only while showScene also searches verticalScenesModel. - toggleEffect now honours Meld's `<layerId>-<index>` legacy id fallback, which the legacy-effect-ids scenario depends on. Tests (61 total, up from 50): - test/replay.test.ts: the replay track is observed so its gain is known before any press, volume steps move by a step rather than collapsing toward silence, presses read the current gain, and the observer is released with the last key. - test/ui-bridge.test.ts: an inspector opening before Meld is up still gets an answer (connected:false drives its notice), connecting and session changes push snapshots, and a new Meld instance's items replace the old ones. registerUiBridge takes an optional connection to allow it. - test/session-toggle.test.ts and test/volume-stepper.test.ts from the previous commit round out the action lifecycle coverage the panel flagged as entirely absent. Every fix above is mutation-verified: reverting it fails the test that covers it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ui/lib/meld-pi.js backs the inspectors of 7 of the 14 actions and had no coverage. Its failures are quiet: a dropdown that lists nothing reads as "Meld has no scenes" rather than a plugin fault, and a settings chain left mismatched produces a key that looks configured but does nothing. test/helpers/fake-dom.ts runs the real file under node:vm against a fake DOM and property-inspector socket. The <select> semantics are reproduced faithfully — assigning a value no option carries leaves selectedIndex at -1, and the first appended option is selected automatically — because meld-pi.js keys its stale-id handling off exactly that rule; a looser fake would hide the bugs the tests exist to catch. 18 tests cover every binding pattern the inspectors use: root and chained selects, the keepFilter show-scene applies to vertical scenes, child lists staying empty until a parent is chosen, the atomic downstream clear when a parent changes, legacy profiles' stale ids falling back to the placeholder without unfiltering the select below, repopulation when a new Meld instance publishes different ids, bindField defaults and writes, watchConnection, malformed frames, and binding ids the markup omits. Mutation-verified: dropping the downstream clear, the stale-id fallback, or the parent guard each fails the covering tests. Suite is now 79 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A review over the code, the tests and the harnesses confirmed 19 issues. Most were tests that did not test what they claimed — the review mutation-tested each one and reported which broke nothing. Plugin fixes: - transition-stepper: the dial offered vertical-canvas scenes. Meld resolves setStagedScene through its horizontal scene model only (unlike showScene, which also searches the vertical one), so staging one is a silent no-op: the dial showed a selection it could not reach and, on press, transitioned somewhere else. Vertical twins are excluded now. - media: the key repainted from an empty session on disconnect, flipping a playing clip to the play icon for the whole reconnect. Every sibling action already held its visual through a blip; this one did not. - show-scene: a press lit the key without checking the scene still exists, faking a confirmation for an id from a previous Meld instance. - toggle-effect: profiles written before effects had stable ids store them as <layerId>-<index>. Meld never publishes that form in the snapshot — it only accepts it as a toggleEffect argument — so the missing-item guard reported those effects absent and left the keys permanently dead after an upgrade, where the legacy plugin worked. That id form resolves now. Harness fidelity (a fake that errs the same way as the plugin hides the bug): - The legacy-effect-id scenario had the contract backwards, publishing <layerId>-<index> as a snapshot key. Effects are published under real ids; legacyEffectId() builds the form a profile stores. - showStagedScene swaps unconditionally, as the QML does. The previous early return absorbed exactly the state the vertical-scene bug reached. - Vertical scenes share index numbering with horizontal ones, as Meld's two separate loops produce. - The fake can now emit gainUpdated without touching the snapshot, which is real timing rather than a shortcut: Meld emits it immediately while the snapshot is republished behind a debounced save. Tests rewritten to observe the rule they name, and mutation-verified: - "a matching echo confirms" passed with the confirmation deleted, since both values render identically; it now asserts the consequence, that a later external change repaints at once. - The mute test now makes the fast path and the snapshot disagree. - Force mode now drives ToggleLayer, which has no direct setter and so actually reaches the idempotency branch ToggleMute skipped. - The blank-stepsize test now checks the gain moved, not just that a call happened; a tautological assertion was removed. - The wrong-peer test uses a connection whose watchdog cannot rescue it. - New coverage for the expiry repaint, the transition dial, media and show-scene. Also: test files ran in parallel, so an ephemeral port freed by one could be claimed by another. The suite is serial now and stable across runs. 91 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The second review round returned 8 findings, down from 19. Plugin fixes: - widget-control: the property inspector offers Wheel Spin -> "Spin it!" and the plugin ships widget-wheelspin-spin artwork, but the icon map had no WHEELSPIN_SPIN entry, so the key rendered the stopwatch-reset icon. The event dispatched correctly; only the label was wrong. Cross-checked every event the inspector can emit against the map; this was the only gap. - show-scene: the optimistic light used the default 750ms TTL, but a scene switch is only confirmed when the session snapshot is republished, and that rides Meld's 600ms retriggerable autosave. Any concurrent session edit resets that timer, so the TTL lost the race and the key blinked dark mid-transition. It now waits 3s, which still self-corrects a press that never took effect. Harness fidelity — each of these let a real regression pass: - toggleLayer/toggleEffect ignored the sceneId/layerId arguments. Meld validates the whole chain (getSceneById, then scene.layers.findById, then the effect within that layer), so a caller sending a stale scene was a no-op there and a success here. Verified: regressing toggle-layer to send the stored scene now fails the covering test, where before it passed. The legacy effect-id fallback also matches the QML now — exactly two dash-separated parts, the index applied to the layer from the arguments, the embedded prefix ignored. - The session snapshot can be republished behind a retriggerable debounce (sessionDebounceMs), modelling the autosave the confirmation actually rides. Off by default so behavioural tests stay fast. - FakeDiv starts hidden, as every shipped inspector's connecting notice does. The previous default made "the notice is showing" true before the runtime ran, so the assertion could not fail. Tests: - watchConnection now asserts the notice is revealed from the hidden state. - New: a show-scene key holds its light across a retriggered debounce, and the widget/legacy paths are covered. - The churn test no longer claims to detect socket leaks it cannot observe; socket closing is covered by the stalled-peer test's clientCount check. All fixes mutation-verified. 92 tests, stable across repeated runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 3 returned 2 findings, both harness infidelities and no plugin defects (19 -> 8 -> 2 across the three rounds). - FakeSelect started enabled, but every scene/layer/effect dropdown ships `disabled` in the inspector markup and bindSessionSelects is the only thing that enables them. A select seeded enabled cannot show that the binding failed to enable it: deleting `el.disabled = false` left the whole suite green while, in the real inspector, those dropdowns would be permanently greyed out and the keys unconfigurable. FakeSelect now defaults to disabled, matching the markup, and a test asserts binding flips it. - callFunction/callFunctionWithArgs were inert in the fake, described as having no observable session effect. That is true for sendCommand and friends but wrong for the two Media uses: play/pause move `isPlaying`, which is published in the layer map and arms the autosave, so a media press really does come back through the snapshot. The fake echoes it now, which makes the press path testable at all — a media press had no coverage, only the initial paint and the reconnect hold. The new test presses toggle and asserts the key converges on the state Meld reports, in both directions. Both mutation-verified: removing the enable line, or inverting the media icon mapping, fails the covering tests. 94 tests, stable across repeated runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 4 returned 9 findings. They are deeper than round 3's because the stricter harness can now see further — several were only reachable once the fake stopped flattering the plugin. The important one is a regression this branch introduced. Meld serves session items as a QVariantMap, which Qt serializes sorted by the 32-hex item id, so snapshot order is unrelated to the order the user sees. The fake had been serving insertion order, which made the legacy `<layerId>-<index>` effect resolution added two commits ago look correct: it indexed into snapshot order, while Meld indexes into its effects model. On a layer with two or more effects that resolves a *different* effect, so an upgraded profile would toggle — and display — the wrong one, where the pre-migration plugin was correct. The id is now forwarded verbatim for Meld to resolve, as that plugin did, and the fake sorts items by id and carries the model order separately so the two can disagree exactly as they do in production. Because a legacy id names an effect whose state the snapshot cannot report, SessionToggleAction grew an `actsWithUnknownState` hook: a press still goes through when the target is identifiable but unreadable, instead of being suppressed by the missing-item guard. Other plugin fixes: - media: `isPlaying` comes from the debounced snapshot, so two toggle presses inside that window both read the pre-press value and sent the same command twice. The pending intent now takes precedence. - media: restart sent seekTo(0), which on a trimmed or looped clip jumps outside the configured region. It uses skipBack, as Meld's own control does, which returns to the loop's start point. Tests, all mutation-verified: - The legacy-effect test uses three effects so the index actually has to be resolved correctly; reintroducing snapshot-order resolution fails it. - Replay's "last key" guard is exercised with two keys mounted — with one key the branch was unreachable and an unconditional release passed. - The echo guard is pinned at the regime its docstring names: gain near the -60 dB floor with a 0.1 dB step, where per-tick deltas are ~1e-5. Widening GAIN_EPSILON to 1e-2 now fails; before, the whole suite passed. - New coverage for the media double-press and skip-back. 98 tests, stable across runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The significant one is the dial-desync class returning through a door this branch never covered. Meld drops every track-observer registration whenever it tears down its audio tracks, and restoring or reopening a session does that WITHOUT closing the socket. Track ids are persisted, so the same ids come back — and registerTrackObserver deduped on "same context, same track", so the re-registration the render issued was swallowed and never reached Meld. No gainUpdated ever arrived again: the dial froze, mute state froze, and the next tick stepped from the stale gain. It stayed broken until the socket dropped or the user rebound the track. Registrations Meld is known to hold are now tracked separately and forgotten on every session change and disconnect, so the next render re-registers. The fake already modelled the trigger (loadScenario clears its observers); nothing had ever driven a dial across it. Other fixes: - media: the optimistic playback state had no expiry repaint, so a press Meld never confirmed left the icon wrong indefinitely. - transition-stepper: dropped any scene whose name contains "(linked)". That was a vestige of when Meld suffixed vertical twins that way; Meld deliberately stopped, so it only discarded scenes a user named that way. The `vertical` flag is the whole test. Tests: - The show-scene "stays lit" test passed with the entire optimistic mechanism deleted: every session republish in its window already carried the confirmed state, so render never had to honour a pending request. It now drives a non-session repaint (a recording change) while the request is pending, which is the only path that reaches that branch, and asserts the key is never painted dark. - The legacy effect-id test used index 1 — the fixed point of the three-element reversal between model order and id-sorted order, so it could not tell them apart. Index 0 discriminates. - New coverage for the session-restore case above. All mutation-verified. 99 tests, stable across runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 6 returned a single finding, and it masked a real inverted-intent bug: the fake republished the session instantly, while Meld only rebuilds the snapshot on `sessionSaved`, behind a ~600ms retriggerable autosave. SessionToggleAction carried no optimistic state, so a second press inside that window re-read the pre-press snapshot. Pressing a "show" key twice sent toggleLayer twice and left the layer hidden — the user asked to show it and it went dark. Reproduced end-to-end before fixing: two presses 200ms apart, layer ends visible:false. Toggles now record the value a press is waiting for, reason from it on a follow-up press, paint it immediately instead of waiting hundreds of milliseconds for confirmation, and expire back to the session if Meld never confirms. The fake's `sessionDebounceMs` now defaults to Meld's real 600ms rather than publishing instantly, so every test runs at production timing. That default is what let this bug hide: five existing tests asserted a repaint within 120ms of a press, which cannot happen against real Meld. They wait for the confirmation now, or assert the optimistic paint where that is the point. New coverage, both mutation-verified: two "show" presses leave the layer visible, and a toggle press repaints without waiting for the snapshot. 101 tests, stable across runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 7 findings. Plugin fix: - media: #findMediaLayer required the stored scene to still own the layer, so an upgraded profile holding a scene from a previous selection made the key silently inert — wrong icon, and every press skipped its callFunction. Meld resolves callFunction by object id alone and never consults a scene, so the check added no safety; toggle-layer and toggle-effect already take the opposite, correct position. Harness fidelity: - mount() installed a frozen iterator, so an instance stayed enumerable in `this.actions` forever. The SDK drops a context from its action store before the plugin's willDisappear handler runs. The visible set is live now, with appear()/disappear() helpers that maintain it. This was not cosmetic: actions stay subscribed to the shared connection for the process's life, so every dial a suite ever mounted kept re-registering on later tests' session updates. Tests that did not test their own subject: - "set replaces the value and restarts the timer" asserted only the value and the expiry count, both of which hold if set() keeps the ORIGINAL timer. That is the invariant a rotation depends on — re-setting on every tick is what holds the optimistic gain across a long rotation — and it was unguarded. It now sleeps past the original deadline. - The session-restore test asserted `registerTrackObserver >= 2` against a counter never reset between tests; it was already 8 before the restore. It now resets and asserts exactly one. - New coverage for the stale-scene media case. All mutation-verified. 102 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three harness gaps from round 7, each of which let a protocol-level regression pass unnoticed. - The fake pushed property updates unconditionally. Qt withholds them until the client sends `idle`, and un-arms the flag with every batch. The fake now gates and coalesces the same way, so a client that stopped acknowledging would visibly stop receiving updates. Verified: removing the client's idle acknowledgement now fails a test, where before the whole suite passed. - `gainUpdated` was broadcast to every socket. Qt only forwards a pure signal to clients that sent `connectToSignal`, so a client that never subscribed would receive nothing in production while the fake fed it everything. Now gated on the subscription — removing the client's subscribe call fails 10 tests. - Tests called `onKeyUp` directly, so `onKeyDown` never ran. The app always delivers both, and `onKeyDown` is what clears the dedupe caches so a press reasserts the key's visuals. A `press()` helper delivers the real sequence and every press now goes through it. 102 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The last two round-7 findings, both tests that named an invariant without exercising it. - The double-press test sampled only the final key state, so a key that flashed back to "hidden" mid-wait was invisible. TOGGLE_CONFIRM_TTL_MS was consequently unguarded suite-wide: shortening it 3000 -> 300 left everything green. The test now asserts the key never goes dark after lighting, and that mutation fails it with the blink visible in the painted sequence. - The replay volume test claimed to protect the unknown-gain guard but could not reach it: beforeEach waits for the observer's gain to land, so the gain was always known. A version that pressed with nothing rendered did not reach it either — it stopped at the trackId check. The track has to be identified while its gain is still in flight, which needs a delayed echo; the test now sets one up, and confirms the same press works once the gain arrives. 103 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erly Round 8 returned 4 findings, three of them regressions from the previous two commits. - The idle gate I added last commit dropped state instead of merging it. Qt's sendPendingPropertyUpdates folds every pending change for an object into one frame carrying all changed properties and their notify signals; mine sent only the newest and discarded the rest. A streaming flag could be stranded false forever with no further event to correct it, and the multi-property frame the client has to decode was never produced. It merges now, and a new test drives three publishes in one turn — under the old behaviour streamingChanged never fires. - show-scene's expiry called renderAll, so one key's timeout cleared every other key's still-pending request and repainted them from the pre-press snapshot. Only the expired context is repainted now. This was reported in round 5 and I fixed the sibling cases but missed this one. - The "never goes dark" assertion added last commit did not pin TOGGLE_CONFIRM_TTL_MS as its comment claimed: it waited out a single undisturbed debounce, so anything above ~650ms passed and the whole 650..3000 band was unguarded. It now retriggers the autosave with a burst of concurrent edits, which is the case the constant exists for; 750 now fails it with the layer left hidden. Both new tests needed a second attempt: the first version of each passed against its own mutation because the scenario could not reach the branch (one queued frame rather than two; a second key whose scene was already current). 105 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 9. The notable one is user-facing. - transition-stepper: the dial press gated only on its own selection, but setStagedScene silently refuses a scene that is already current, and the dial's list can be a snapshot behind after a scene switch made elsewhere in Meld. showStagedScene swaps current and staged unconditionally, so acting on a refused selection made Meld adopt an unset staged scene and left the session with no current scene at all. The snapshot now has to confirm the stage before the press is forwarded. - media and the shared toggle base kept a pending press keyed by context only, so retargeting a key within the confirmation window applied the previous item's pending value to the new one: the key showed the inverse of reality and the next press moved the property the wrong way. VolumeStepper already guarded this via onSettingsChanged; both now do. Harness: - Staged-scene changes republish immediately. Meld wires stagedSceneChanged straight to the mapper, so unlike scene/layer/mute edits it does not ride the autosave. Debouncing it meant no snapshot ever landed during a rotation, leaving the transition dial's optimistic reconciliation unreachable from the suite. - The transition tests waited 40ms for a scenario reload that rides a 600ms debounce, so each test reasoned from the previous test's state. - "delivers recording updates" asserted `true` when the previous test had already left recording true, and the fake published a true->true no-op Qt would never emit. It now drives a real transition in both directions; hardcoding the emitted boolean fails it. All mutation-verified. Two of the new tests needed a second attempt: the staged-guard test first blocked at an earlier guard, and the recording test needed a genuinely false baseline. 106 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 10. - media: the key never repainted on a settings change while Meld was disconnected, so changing the press mode left the previous mode's artwork until reconnect. For the fixed-icon modes the setting alone determines the artwork, so that render is now allowed through. - No test ever delivered didReceiveSettings, leaving the retarget guards added last round entirely uncovered. A retarget() helper delivers it, and both media and the toggles now cover a pending press being dropped when the key is pointed at a different item. - MEDIA_CONFIRM_TTL_MS was unpinned: 3000 -> 300 left the whole suite green, while both sibling constants were guarded by a retriggering edit burst. Media now has the same. - SessionToggleAction.render's pending-press precedence survived removal, so a non-session repaint during the wait could light the key back to the inverse of the press. - session-toggle's beforeEach waited 40ms for a scenario reload that rides the 600ms debounce, so every test in the file reasoned from the previous test's state — the same defect fixed in the transition suite last round. 110 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Align the plugin manifest, npm package, and lockfile on release version 0.6.4.0. Document the Node SDK refactor, shared Meld connection, improved reconnect state synchronization, and configurable Record Clip durations from 0 to 90 seconds. Update the Stream Deck 7.1 requirement and describe the versioned release artifact naming convention.
Cue, Layer Visibility, Effect Visibility, Record, Go Live, Change Scene, and Mute Track were painting the wrong key state for their active/inactive condition, which showed custom icon packs backwards. State 0 is now consistently the active/on icon and state 1 the inactive/off icon across all toggle actions, with Mute Track's "active" defined as unmuted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.