Skip to content

Commit 70d4ed8

Browse files
committed
Clarify MIDI Studio V2 launch mode save ownership between Workspace and tool-only workflows - PR_26146_048-midi-studio-v2-launch-mode-save-ownership
1 parent 1a0da18 commit 70d4ed8

6 files changed

Lines changed: 197 additions & 4 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# PR_26146_048 MIDI Studio V2 Launch Mode Save Ownership Validation
2+
3+
Status: PASS
4+
5+
## Scope
6+
- Continued from PR_26146_047.
7+
- Added visible launch mode indicator for Tool Mode and Workspace Mode.
8+
- Preserved Tool Mode standalone Import JSON, Save Project, and output export workflow.
9+
- Kept Workspace Manager launch mode focused on Return to Workspace while hiding standalone project save/import/export controls.
10+
- Relabeled rendered export controls as output export (`Output Type`, `Save Output`) so they are not mistaken for project save.
11+
- Verified Workspace edits update the active canonical MIDI Studio tool payload in session storage before returning to Workspace Manager.
12+
13+
## Changed-File Syntax Checks
14+
PASS:
15+
16+
```text
17+
node --check tools/midi-studio-v2/js/controls/ActionNavControl.js
18+
node --check tools/midi-studio-v2/js/bootstrap.js
19+
node --check tests/playwright/tools/MidiStudioV2.spec.mjs
20+
node --check tools/midi-studio-v2/js/MidiStudioV2App.js
21+
```
22+
23+
Additional guards:
24+
25+
```text
26+
HTML external-only guard passed
27+
CSS brace check passed
28+
```
29+
30+
## Targeted Playwright Validation
31+
PASS:
32+
33+
```text
34+
npx.cmd playwright test tests/playwright/tools/MidiStudioV2.spec.mjs -g "separates Workspace launch save ownership from Tool Mode standalone save|launches and renders a valid multi-song manifest payload|exports output through Type dropdown and Save Output without claiming project save|persists octave note edits into canonical song data, playback, save, and reset|fast octave note editing supports drag painting keyboard shortcuts selection and timeline scroll sync" --config=codex_playwright_system_chrome.config.cjs --reporter=list --workers=1 --timeout=60000
35+
```
36+
37+
Result:
38+
39+
```text
40+
Running 5 tests using 1 worker
41+
ok 1 fast octave note editing supports drag painting keyboard shortcuts selection and timeline scroll sync
42+
ok 2 persists octave note edits into canonical song data, playback, save, and reset
43+
ok 3 separates Workspace launch save ownership from Tool Mode standalone save
44+
ok 4 launches and renders a valid multi-song manifest payload
45+
ok 5 exports output through Type dropdown and Save Output without claiming project save
46+
5 passed
47+
```
48+
49+
## Required Assertions Covered
50+
- PASS: Workspace launch shows Return to Workspace.
51+
- PASS: Workspace launch hides standalone Save Project and Import JSON controls.
52+
- PASS: Workspace edits remain in the canonical MIDI Studio tool payload before return.
53+
- PASS: Tool-only launch shows Import JSON and Save Project.
54+
- PASS: Export Type + Save Output remains output-only and does not report project save.
55+
- PASS: Play and Stop still work through targeted playback/editing coverage.
56+
57+
## Diff Hygiene
58+
PASS:
59+
60+
```text
61+
git diff --check
62+
```
63+
64+
Git reported line-ending normalization warnings for touched files, but no whitespace errors.
65+
66+
## Not Run
67+
- Full samples smoke test was not run, per PR instructions.
68+
69+
## Result
70+
PR_26146_048 is marked PASS. Workspace Manager owns final save in Workspace Mode, while Tool Mode keeps the standalone file workflow.

tests/playwright/tools/MidiStudioV2.spec.mjs

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,50 @@ async function openMidiStudioForImport(page, audioOptions = {}) {
298298
return server;
299299
}
300300

301+
async function openMidiStudioFromWorkspace(page, manifestPayload, audioOptions = {}) {
302+
const server = await startRepoServer();
303+
const hostContextId = "midi-studio-v2-workspace-context";
304+
await installMockAudio(page, audioOptions);
305+
await page.addInitScript(({ hostContextId: contextId, manifest }) => {
306+
const toolId = "midi-studio-v2";
307+
const toolSettings = manifest.tools?.[toolId] || {};
308+
const payload = {
309+
schema: "html-js-gaming.midi-studio-v2",
310+
toolId,
311+
version: manifest.music?.version || 1,
312+
runtimePreference: manifest.music?.runtimePreference || "rendered",
313+
activeSongId: toolSettings.activeSongId || manifest.music?.activeSongId || manifest.music?.songs?.[0]?.id || "",
314+
directorMode: toolSettings.directorMode || manifest.music?.directorMode || {},
315+
songs: manifest.music?.songs || []
316+
};
317+
window.sessionStorage.setItem(contextId, JSON.stringify({
318+
...manifest,
319+
tools: {
320+
...(manifest.tools || {}),
321+
[toolId]: payload
322+
}
323+
}));
324+
window.sessionStorage.setItem(`workspace.tools.${toolId}`, JSON.stringify({
325+
schema: { name: "workspace-tool-state", version: 1 },
326+
workspace: {
327+
hostContextId: contextId,
328+
source: "workspace-manager-v2",
329+
toolId
330+
},
331+
data: payload,
332+
dirty: {
333+
isDirty: false,
334+
reason: "clean",
335+
changedAt: "",
336+
changedKeys: []
337+
}
338+
}));
339+
}, { hostContextId, manifest: manifestPayload });
340+
await workspaceV2CoverageReporter.start(page);
341+
await page.goto(`${server.baseUrl}/tools/midi-studio-v2/index.html?launch=workspace&fromTool=workspace-manager-v2&hostContextId=${hostContextId}&workspaceMode=uat`, { waitUntil: "domcontentloaded" });
342+
return server;
343+
}
344+
301345
async function selectMidiStudioTab(page, tabId) {
302346
const tab = page.locator(`[data-midi-studio-tab="${tabId}"]`);
303347
if (await tab.getAttribute("aria-selected") === "true") {
@@ -1028,6 +1072,52 @@ test.describe("MIDI Studio V2", () => {
10281072
}
10291073
});
10301074

1075+
test("separates Workspace launch save ownership from Tool Mode standalone save", async ({ page }) => {
1076+
const manifest = JSON.parse(await fs.readFile(uatManifestPath, "utf8"));
1077+
const server = await openMidiStudioFromWorkspace(page, manifest);
1078+
try {
1079+
await expect(page.locator("#launchModeIndicator")).toHaveText("Workspace Mode");
1080+
await expect(page.locator("body")).toHaveAttribute("data-midi-studio-launch-mode", "workspace");
1081+
await expect(page.locator('[data-launch-mode-nav="workspace"]')).toBeVisible();
1082+
await expect(page.locator("#returnToWorkspaceButton")).toBeVisible();
1083+
await expect(page.locator("#saveProjectButton")).toBeHidden();
1084+
await expect(page.locator("#toolImportManifestButton")).toBeHidden();
1085+
await expect(page.locator("#renderedExportSaveButton")).toBeHidden();
1086+
await expect(page.locator("#workspaceImportManifestButton")).toBeHidden();
1087+
await expect(page.locator("#workspaceCopyManifestButton")).toBeHidden();
1088+
await expect(page.locator("#workspaceExportManifestButton")).toBeHidden();
1089+
1090+
await selectInstrumentRow(page, "lead");
1091+
await octaveCell(page, "C6", 2).click();
1092+
await expect(octaveCell(page, "C6", 2)).toHaveAttribute("data-note-lanes", /lead/);
1093+
const workspaceState = await page.evaluate(() => {
1094+
const session = JSON.parse(window.sessionStorage.getItem("workspace.tools.midi-studio-v2"));
1095+
const context = JSON.parse(window.sessionStorage.getItem("midi-studio-v2-workspace-context"));
1096+
const sessionSong = session.data.songs.find((song) => song.id === session.data.activeSongId);
1097+
const contextSong = context.tools["midi-studio-v2"].songs.find((song) => song.id === context.tools["midi-studio-v2"].activeSongId);
1098+
return {
1099+
contextLead: contextSong.studioArrangement.lanes.lead,
1100+
dirty: session.dirty,
1101+
sessionLead: sessionSong.studioArrangement.lanes.lead
1102+
};
1103+
});
1104+
expect(workspaceState.sessionLead).toContain("C6");
1105+
expect(workspaceState.contextLead).toContain("C6");
1106+
expect(workspaceState.dirty).toMatchObject({
1107+
isDirty: true,
1108+
reason: "midi-studio-note-grid-edited"
1109+
});
1110+
expect(workspaceState.dirty.changedKeys).toEqual(expect.arrayContaining(["data.songs.studioArrangement"]));
1111+
1112+
await page.locator("#returnToWorkspaceButton").click();
1113+
await expect(page).toHaveURL(/workspace-manager-v2\/index\.html.*hostContextId=midi-studio-v2-workspace-context/);
1114+
await expect(page).toHaveURL(/workspace=uat/);
1115+
} finally {
1116+
await workspaceV2CoverageReporter.stop(page);
1117+
await server.close();
1118+
}
1119+
});
1120+
10311121
test("octave timeline freezes compact headers and note labels while active cells stay textless", async ({ page }) => {
10321122
const server = await openMidiStudioForImport(page);
10331123
try {
@@ -1780,6 +1870,8 @@ test.describe("MIDI Studio V2", () => {
17801870
const server = await openMidiStudio(page);
17811871
try {
17821872
await expect(page.locator("body")).toHaveAttribute("data-tool-id", "midi-studio-v2");
1873+
await expect(page.locator("body")).toHaveAttribute("data-midi-studio-launch-mode", "tool");
1874+
await expect(page.locator("#launchModeIndicator")).toHaveText("Tool Mode");
17831875
await expect(page.locator("[data-midi-studio-header]")).toContainText("MIDI Studio V2");
17841876
await expect(page.locator("#songList [data-song-id]")).toHaveCount(3);
17851877
await expect(page.locator('[data-song-id="theme-main"]')).toHaveAttribute("aria-pressed", "true");
@@ -1796,11 +1888,13 @@ test.describe("MIDI Studio V2", () => {
17961888
await expect(page.locator("#exportMp3Button")).toHaveCount(0);
17971889
await expect(page.locator("#exportOggButton")).toHaveCount(0);
17981890
await expect(page.locator(".midi-studio-v2__tool-menu #toolImportManifestButton")).toBeVisible();
1891+
await expect(page.locator(".midi-studio-v2__tool-menu #saveProjectButton")).toBeVisible();
17991892
await expect(page.locator(".midi-studio-v2__tool-menu #loadExampleAndPlayButton")).toHaveCount(0);
18001893
await expect(page.locator(".midi-studio-v2__tool-menu #stopAllAudioButton")).toBeVisible();
1801-
await expect(page.locator('.midi-studio-v2__tool-menu label[for="renderedExportTargetTypeSelect"]')).toContainText("Type");
1894+
await expect(page.locator('.midi-studio-v2__tool-menu label[for="renderedExportTargetTypeSelect"]')).toContainText("Output Type");
18021895
await expect(page.locator(".midi-studio-v2__tool-menu #renderedExportTargetTypeSelect")).toBeVisible();
18031896
await expect(page.locator(".midi-studio-v2__tool-menu #renderedExportSaveButton")).toBeVisible();
1897+
await expect(page.locator(".midi-studio-v2__tool-menu #renderedExportSaveButton")).toHaveText("Save Output");
18041898
await expect(page.locator("#midiSourceDetails")).toContainText("No MIDI source inspected.");
18051899
await expect(page.locator("#audioDiagnosticsContent")).toBeHidden();
18061900
await expect(page.locator("#playbackState")).toContainText("Audible preview ready: Main Theme.");
@@ -1925,7 +2019,7 @@ test.describe("MIDI Studio V2", () => {
19252019
}
19262020
});
19272021

1928-
test("exports through Type dropdown and Save without claiming files were written", async ({ page }) => {
2022+
test("exports output through Type dropdown and Save Output without claiming project save", async ({ page }) => {
19292023
const server = await openMidiStudio(page);
19302024
try {
19312025
await expect(page.locator("#exportWavButton")).toHaveCount(0);
@@ -1934,6 +2028,7 @@ test.describe("MIDI Studio V2", () => {
19342028
await expect(page.locator("#renderedExportTargetTypeSelect")).toBeVisible();
19352029
await expect(page.locator("#renderedExportTargetTypeSelect option")).toContainText(["WAV", "MP3", "OGG"]);
19362030
await expect(page.locator("#renderedExportSaveButton")).toBeVisible();
2031+
await expect(page.locator("#renderedExportSaveButton")).toHaveText("Save Output");
19372032
const exportControlsFit = await page.locator(".midi-studio-v2__tool-menu").evaluate((menu) => {
19382033
const label = menu.querySelector('label[for="renderedExportTargetTypeSelect"]').getBoundingClientRect();
19392034
const typeSelect = menu.querySelector("#renderedExportTargetTypeSelect").getBoundingClientRect();
@@ -1955,6 +2050,7 @@ test.describe("MIDI Studio V2", () => {
19552050
await expect(page.locator("#statusLog")).toHaveValue(/WARN Export rendering not implemented for WAV\. Planned target: assets\/music\/rendered\/theme-main\.wav\./);
19562051
await expect(page.locator("#statusLog")).toHaveValue(/WARN Export rendering not implemented for MP3\. Planned target: assets\/music\/rendered\/theme-main\.mp3\./);
19572052
await expect(page.locator("#statusLog")).toHaveValue(/WARN Export rendering not implemented for OGG\. Planned target: assets\/music\/rendered\/theme-main\.ogg\./);
2053+
await expect(page.locator("#statusLog")).not.toHaveValue(/Save Project completed/);
19582054
await page.locator('[data-song-id="source-only"]').click();
19592055
await page.locator("#renderedExportTargetTypeSelect").selectOption("wav");
19602056
await page.locator("#renderedExportSaveButton").click();

tools/midi-studio-v2/index.html

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,14 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
4949
<span id="projectDirtyState" class="midi-studio-v2__dirty-state" data-midi-studio-dirty-state="clean">Saved</span>
5050
<div class="midi-studio-v2__export-save-controls">
5151
<label class="midi-studio-v2__compact-field" for="renderedExportTargetTypeSelect">
52-
<span>Type</span>
52+
<span>Output Type</span>
5353
<select id="renderedExportTargetTypeSelect">
5454
<option value="wav">WAV</option>
5555
<option value="mp3">MP3</option>
5656
<option value="ogg">OGG</option>
5757
</select>
5858
</label>
59-
<button id="renderedExportSaveButton" type="button">Save</button>
59+
<button id="renderedExportSaveButton" type="button">Save Output</button>
6060
</div>
6161
<button id="toolExportToolStateButton" type="button" disabled>Export JSON</button>
6262
<input id="toolImportManifestInput" type="file" accept="application/json,.json" hidden>
@@ -69,6 +69,10 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
6969
<button id="returnToWorkspaceButton" type="button" hidden>Return to Workspace</button>
7070
</nav>
7171

72+
<div class="midi-studio-v2__mode-bar">
73+
<span id="launchModeIndicator" class="midi-studio-v2__mode-indicator">Tool Mode</span>
74+
</div>
75+
7276
<nav class="midi-studio-v2__tabs" aria-label="MIDI Studio sections" role="tablist">
7377
<button class="is-active" type="button" role="tab" aria-selected="true" data-midi-studio-tab="studio">Studio</button>
7478
<button type="button" role="tab" aria-selected="false" data-midi-studio-tab="song-setup">Song Setup</button>

tools/midi-studio-v2/js/bootstrap.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ window.addEventListener("DOMContentLoaded", () => {
4141
const app = new MidiStudioV2App({
4242
accordions: Array.from(document.querySelectorAll(".accordion-v2"), (section) => new AccordionSection(section)),
4343
actionNav: new ActionNavControl({
44+
launchModeIndicator: requireElement("#launchModeIndicator"),
4445
nowPlayingLabel: requireElement("#nowPlayingLabel"),
4546
projectDirtyState: requireElement("#projectDirtyState"),
4647
returnToWorkspaceButton: requireElement("#returnToWorkspaceButton"),

tools/midi-studio-v2/js/controls/ActionNavControl.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export class ActionNavControl {
22
constructor({
33
locationRef = window.location,
4+
launchModeIndicator,
45
nowPlayingLabel,
56
projectDirtyState,
67
returnToWorkspaceButton,
@@ -19,6 +20,7 @@ export class ActionNavControl {
1920
workspaceNav
2021
}) {
2122
this.location = locationRef;
23+
this.launchModeIndicator = launchModeIndicator;
2224
this.nowPlayingLabel = nowPlayingLabel;
2325
this.projectDirtyState = projectDirtyState;
2426
this.returnToWorkspaceButton = returnToWorkspaceButton;
@@ -74,6 +76,11 @@ export class ActionNavControl {
7476
this.workspaceCopyManifestButton.hidden = isWorkspaceManagerLaunch;
7577
this.workspaceExportManifestButton.hidden = isWorkspaceManagerLaunch;
7678
this.returnToWorkspaceButton.hidden = !isWorkspaceManagerLaunch;
79+
if (this.launchModeIndicator) {
80+
this.launchModeIndicator.textContent = isWorkspace ? "Workspace Mode" : "Tool Mode";
81+
this.launchModeIndicator.dataset.midiStudioLaunchMode = isWorkspace ? "workspace" : "tool";
82+
}
83+
this.window.document.body.dataset.midiStudioLaunchMode = isWorkspace ? "workspace" : "tool";
7784
}
7885

7986
setToolActionsEnabled(isEnabled) {

tools/midi-studio-v2/styles/midiStudioV2.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,21 @@
106106
border-color: rgba(245, 158, 11, 0.72);
107107
}
108108

109+
.midi-studio-v2__mode-bar {
110+
display: flex;
111+
justify-content: flex-end;
112+
padding: 0.4rem 1rem 0;
113+
}
114+
115+
.midi-studio-v2__mode-indicator {
116+
border: 1px solid var(--tool-starter-line);
117+
border-radius: 999px;
118+
font-size: 0.78rem;
119+
font-weight: 800;
120+
padding: 0.22rem 0.6rem;
121+
text-transform: uppercase;
122+
}
123+
109124
.midi-studio-v2.app-shell {
110125
grid-template-columns: 350px minmax(0, 1fr) minmax(14rem, 18rem);
111126
}

0 commit comments

Comments
 (0)