Skip to content

feat: add V2 workflow state backbone#912

Closed
ChuxiJ wants to merge 1 commit intocodex/v2-905-design-systemfrom
codex/v2-906-workflow-model
Closed

feat: add V2 workflow state backbone#912
ChuxiJ wants to merge 1 commit intocodex/v2-905-design-systemfrom
codex/v2-906-workflow-model

Conversation

@ChuxiJ
Copy link
Contributor

@ChuxiJ ChuxiJ commented Mar 20, 2026

Summary

  • introduce first-class V2 transport and workflow mode enums for the VST3 plugin state model
  • add session metadata and richer per-take result objects while keeping the existing MVP fields mirrored for compatibility
  • update processor workflow transitions so generation, poll, preview download, and persistence populate the new V2 backbone without changing the shipped MVP UI path

Validation

  • cmake --build build/acestep_vst3 --config Release
  • synced the rebuilt bundle to ~/Library/Audio/Plug-Ins/VST3/ACE-Step VST3 Shell.vst3 for Reaper smoke testing

Closes #906
Refs #903

Summary by CodeRabbit

  • New Features

    • New UI: larger, redesigned editor with modern chrome, status strip, synth panel, transport controls, result deck and preview deck (load/play/clear/reveal).
    • Workflow modes and transport states exposed as human-readable names (including "Compare Ready"); improved session naming and compare workflows.
    • Rich per-result metadata and preview handling (remote/local URLs, seed/duration/model/quality, ready-for-compare).
  • Chores

    • Plugin state version bumped to 2 (state serialization updated).

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

Caution

Review failed

Failed to post review comments

📝 Walkthrough

Walkthrough

Introduces a V2 workflow model: new enums (transport/workflow), per-session and per-take state, state version bump to 2, XML serialization/migration for legacy fields, processor lifecycle/state transitions, and new V2 UI components and look-and-feel for session/transport/result/preview panels.

Changes

Cohort / File(s) Summary
Build / Version
plugins/acestep_vst3/src/PluginConfig.h
Bumped compile-time state version kCurrentStateVersion 1 → 2.
Enums & Converters
plugins/acestep_vst3/src/PluginEnums.h, plugins/acestep_vst3/src/PluginEnums.cpp
Added TransportState and WorkflowMode enums plus bidirectional string conversion/parsing with tolerant normalization and variant spellings.
State Types
plugins/acestep_vst3/src/PluginState.h
Added PluginResultTake and PluginSessionState; extended PluginState with transportState, workflowMode, session, and resultTakes array.
State I/O & Migration
plugins/acestep_vst3/src/PluginState.cpp
Extended XML serialization/deserialization for new top-level/session/take fields; added import/sync helpers to reconcile legacy per-slot arrays with new resultTakes; derived transportState from jobStatus when idle.
Processor Logic
plugins/acestep_vst3/src/PluginProcessor.cpp
Updated generation lifecycle: set session name, enforce failed/submitting/queued/rendering/compareReady/succeeded transitions; populate/mirror resultTakes into legacy arrays; update lastCompletedSlot and compare slot selection; refresh session cancelability.
Editor / State Wiring
plugins/acestep_vst3/src/PluginEditor.cpp, plugins/acestep_vst3/src/PluginEditor.h, plugins/acestep_vst3/src/PluginEditorState.cpp
Replaced many low-level widgets with composed V2 components (StatusStrip, SynthPanel, TapeTransport, ResultDeck, PreviewDeck) and rewired state sync to use the new components and session/take fields.
New UI Components
plugins/acestep_vst3/src/PreviewDeckComponent.*, ResultDeckComponent.*, StatusStripComponent.*, SynthPanelComponent.*, TapeTransportComponent.*
Added multiple JUCE components implementing preview, result deck, status strip, synth panel, and tape-transport UI with accessors for controls used by editor/state glue.
V2 Chrome & LookAndFeel
plugins/acestep_vst3/src/V2Chrome.*, V2LookAndFeel.*
Added V2 drawing helpers, color palette constants, and a LookAndFeel that customizes button/combobox/text-editor rendering and fonts.
CMake
plugins/acestep_vst3/CMakeLists.txt
Added new source files to the plugin target (new components, V2 chrome/look-and-feel).

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Editor
participant Processor
participant Backend
participant StateStore
Editor->>Processor: requestGeneration(prompt)
Processor->>StateStore: set transportState=submitting; sessionName
Processor->>Backend: submit job
alt submission failed
Backend-->>Processor: error
Processor->>StateStore: set transportState=failed
Processor->>Editor: refresh UI
else submission accepted
Backend-->>Processor: jobId
Processor->>StateStore: set transportState=queued
loop polling
Processor->>Backend: poll jobId
Backend-->>Processor: status/progress/result metadata
alt failure
Processor->>StateStore: set transportState=failed
break
else running/queued
Processor->>StateStore: set transportState=queued/rendering
end
alt completed with results
Processor->>StateStore: populate resultTakes[], mirror legacy arrays, set lastCompletedSlot/compare slots
Processor->>StateStore: set transportState=compareReady/succeeded
break
end
end
Processor->>Editor: refresh UI (status, result lists)
end

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Poem

🐰 I hopped from Idle to Queue with a twitch,
Submitting seeds, takes bundled and rich,
Sessions named, compares set to play,
Legacy fields wink as new ones stay,
V2 hums — a tape-synth carrot-rich! 🎶

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR introduces V2 UI chrome components (V2LookAndFeel, StatusStripComponent, TapeTransportComponent, ResultDeckComponent, PreviewDeckComponent, SynthPanelComponent) and updates PluginEditor/PluginEditorState for V2 UI integration, which are out of scope for the workflow state backbone requirement. Separate V2 UI chrome and editor refactoring into a distinct PR focused on UI implementation, keeping this PR focused purely on workflow state model and processor logic.
Docstring Coverage ⚠️ Warning Docstring coverage is 2.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the main change: introducing a V2 workflow state backbone, which aligns with the primary objective of expanding plugin state structure.
Linked Issues check ✅ Passed The PR meets all coding requirements from issue #906: introduces TransportState/WorkflowMode enums [#906], adds PluginSessionState and PluginResultTake structs with rich metadata [#906], implements queue-aware transport states [#906], and establishes multi-take history as first-class workflow elements [#906].

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/v2-906-workflow-model

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
plugins/acestep_vst3/src/PluginEnums.cpp (1)

176-178: Consider adding "compareready" (no delimiter) variant for consistency.

The jobStatusFromString function accepts variants like "queued/running" without delimiters, but transportStateFromString only handles "compare ready", "compare_ready", and "compare-ready". Adding "compareready" would improve robustness if users paste values without delimiters.

♻️ Suggested addition
-    if (key == "compare ready" || key == "compare_ready" || key == "compare-ready")
+    if (key == "compare ready" || key == "compare_ready" || key == "compare-ready" || key == "compareready")
     {
         return TransportState::compareReady;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/acestep_vst3/src/PluginEnums.cpp` around lines 176 - 178, In
transportStateFromString, add the delimiter-less variant "compareready" to the
set of accepted keys so TransportState::compareReady is returned for inputs
without spaces or punctuation; update the branch that currently checks for
"compare ready", "compare_ready", and "compare-ready" to also accept
"compareready" to make parsing more robust.
plugins/acestep_vst3/src/PluginProcessor.cpp (2)

20-27: Add bounds check to mirrorTakeIntoLegacyFields for defensive coding.

The function casts slotIndex to size_t without validating it's within [0, kResultSlotCount). While current callers appear to pass valid indices, a negative or out-of-range value would cause undefined behavior.

🛡️ Proposed fix
 void mirrorTakeIntoLegacyFields(PluginState& state, int slotIndex)
 {
+    if (slotIndex < 0 || slotIndex >= kResultSlotCount)
+    {
+        return;
+    }
     const auto index = static_cast<size_t>(slotIndex);
     const auto& take = state.resultTakes[index];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/acestep_vst3/src/PluginProcessor.cpp` around lines 20 - 27, The
function mirrorTakeIntoLegacyFields currently casts slotIndex to size_t and
indexes into arrays without validation; add a defensive bounds check at the
start of mirrorTakeIntoLegacyFields to ensure slotIndex is >= 0 and less than
the valid slot count (use kResultSlotCount or validate against
state.resultTakes.size()/state.resultSlots.size()), and early-return (optionally
log) if out-of-range to avoid undefined behavior when accessing
state.resultTakes, state.resultSlots, state.resultFileUrls, or
state.resultLocalPaths.

391-392: schedulePreviewDownload reads from legacy resultFileUrls instead of resultTakes.

The V2 code populates resultTakes[].remoteFileUrl in applyCompletedTask, then mirrors to legacy arrays. However, schedulePreviewDownload reads directly from state_.resultFileUrls. If called before mirroring completes (e.g., via pendingPreviewDownloadSlot_ set at line 498), this could use stale data.

Consider reading from resultTakes[].remoteFileUrl for consistency with the V2 model.

♻️ Suggested fix
     const auto baseUrl = state_.backendBaseUrl;
-    const auto remoteFileUrl = state_.resultFileUrls[static_cast<size_t>(slotIndex)];
+    const auto& take = state_.resultTakes[static_cast<size_t>(slotIndex)];
+    const auto remoteFileUrl = take.remoteFileUrl.isNotEmpty()
+                                   ? take.remoteFileUrl
+                                   : state_.resultFileUrls[static_cast<size_t>(slotIndex)];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/acestep_vst3/src/PluginProcessor.cpp` around lines 391 - 392,
schedulePreviewDownload currently reads legacy state_.resultFileUrls which can
be stale; update it to read the V2 field
state_.resultTakes[slotIndex].remoteFileUrl (use that as the primary source) and
only fall back to state_.resultFileUrls[static_cast<size_t>(slotIndex)] if the
take entry is empty/null. Locate schedulePreviewDownload and replace the
remoteFileUrl lookup, ensuring behavior remains compatible with
applyCompletedTask mirroring and the pendingPreviewDownloadSlot_ flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@plugins/acestep_vst3/src/PluginState.cpp`:
- Around line 179-197: The migration currently remaps state.transportState
whenever it equals TransportState::idle, which can incorrectly override
legitimate v2 idle states; update the migration in PluginState.cpp to only
perform this derivation for legacy states by gating it on schemaVersion < 2 (or
by checking that the transportState XML attribute was actually missing during
deserialization) before inspecting state.jobStatus, and leave transportState
unchanged for true v2 states; reference the transportState/jobStatus logic and
the schemaVersion check in the deserialization/migration area to implement this
conditional gating.

---

Nitpick comments:
In `@plugins/acestep_vst3/src/PluginEnums.cpp`:
- Around line 176-178: In transportStateFromString, add the delimiter-less
variant "compareready" to the set of accepted keys so
TransportState::compareReady is returned for inputs without spaces or
punctuation; update the branch that currently checks for "compare ready",
"compare_ready", and "compare-ready" to also accept "compareready" to make
parsing more robust.

In `@plugins/acestep_vst3/src/PluginProcessor.cpp`:
- Around line 20-27: The function mirrorTakeIntoLegacyFields currently casts
slotIndex to size_t and indexes into arrays without validation; add a defensive
bounds check at the start of mirrorTakeIntoLegacyFields to ensure slotIndex is
>= 0 and less than the valid slot count (use kResultSlotCount or validate
against state.resultTakes.size()/state.resultSlots.size()), and early-return
(optionally log) if out-of-range to avoid undefined behavior when accessing
state.resultTakes, state.resultSlots, state.resultFileUrls, or
state.resultLocalPaths.
- Around line 391-392: schedulePreviewDownload currently reads legacy
state_.resultFileUrls which can be stale; update it to read the V2 field
state_.resultTakes[slotIndex].remoteFileUrl (use that as the primary source) and
only fall back to state_.resultFileUrls[static_cast<size_t>(slotIndex)] if the
take entry is empty/null. Locate schedulePreviewDownload and replace the
remoteFileUrl lookup, ensuring behavior remains compatible with
applyCompletedTask mirroring and the pendingPreviewDownloadSlot_ flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aac52be8-9075-4900-82f0-34c6f6729b60

📥 Commits

Reviewing files that changed from the base of the PR and between 00f514a and 265cf39.

📒 Files selected for processing (6)
  • plugins/acestep_vst3/src/PluginConfig.h
  • plugins/acestep_vst3/src/PluginEnums.cpp
  • plugins/acestep_vst3/src/PluginEnums.h
  • plugins/acestep_vst3/src/PluginProcessor.cpp
  • plugins/acestep_vst3/src/PluginState.cpp
  • plugins/acestep_vst3/src/PluginState.h

Comment on lines +179 to +197
if (state.transportState == TransportState::idle)
{
if (state.jobStatus == JobStatus::submitting)
{
state.transportState = TransportState::submitting;
}
else if (state.jobStatus == JobStatus::queuedOrRunning)
{
state.transportState = TransportState::rendering;
}
else if (state.jobStatus == JobStatus::succeeded)
{
state.transportState = TransportState::succeeded;
}
else if (state.jobStatus == JobStatus::failed)
{
state.transportState = TransportState::failed;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Migration logic may incorrectly override v2 states where transportState is legitimately idle.

This derivation runs whenever transportState == idle, not only for v1 states (where the transportState attribute is absent). If a v2 state has transportState=idle but jobStatus is non-idle (e.g., after a partial state manipulation), this logic will incorrectly override transportState.

Consider gating this migration on schemaVersion < 2 or checking whether the transportState attribute was actually absent in the XML.

🛡️ Suggested fix
     importLegacyResultFields(state);
     syncLegacyResultFields(state);
-    if (state.transportState == TransportState::idle)
+    // Only derive transportState from jobStatus when migrating v1 state
+    if (state.schemaVersion < 2 && state.transportState == TransportState::idle)
     {
         if (state.jobStatus == JobStatus::submitting)
         {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/acestep_vst3/src/PluginState.cpp` around lines 179 - 197, The
migration currently remaps state.transportState whenever it equals
TransportState::idle, which can incorrectly override legitimate v2 idle states;
update the migration in PluginState.cpp to only perform this derivation for
legacy states by gating it on schemaVersion < 2 (or by checking that the
transportState XML attribute was actually missing during deserialization) before
inspecting state.jobStatus, and leave transportState unchanged for true v2
states; reference the transportState/jobStatus logic and the schemaVersion check
in the deserialization/migration area to implement this conditional gating.

@ChuxiJ ChuxiJ mentioned this pull request Mar 20, 2026
6 tasks
@ChuxiJ
Copy link
Contributor Author

ChuxiJ commented Mar 21, 2026

Superseded by consolidated V2 implementation PR #919, which now carries this stack forward against . Closing this split PR to keep the review surface clean.

@ChuxiJ ChuxiJ closed this Mar 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tape Synth V2: Full workflow model

1 participant