@signalsandsorcery/plugin-sdk 2.34.1 → 2.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -4753,6 +4753,9 @@ function synthesizeCuePoints({
4753
4753
  };
4754
4754
  }
4755
4755
 
4756
+ // src/panel-core/useGeneratorPanelCore.tsx
4757
+ import { useState as useState19, useEffect as useEffect16, useCallback as useCallback14, useRef as useRef17, useMemo as useMemo8 } from "react";
4758
+
4756
4759
  // src/hooks/useSceneState.ts
4757
4760
  import { useState as useState15, useCallback as useCallback11, useRef as useRef14 } from "react";
4758
4761
  function useSceneState(activeSceneId, initialValue) {
@@ -4914,8 +4917,2326 @@ function useSoundHistory(applySound, opts = {}) {
4914
4917
  );
4915
4918
  }
4916
4919
 
4920
+ // src/panel-core/track-state.ts
4921
+ function newTrackState(handle, overrides = {}) {
4922
+ return {
4923
+ handle,
4924
+ prompt: "",
4925
+ role: "",
4926
+ runtimeState: { id: handle.id, muted: false, solo: false, volume: 0.75, pan: 0 },
4927
+ fxDetailState: { ...EMPTY_FX_DETAIL_STATE },
4928
+ drawerOpen: false,
4929
+ drawerTab: "fx",
4930
+ editorStage: false,
4931
+ isGenerating: false,
4932
+ error: null,
4933
+ hasMidi: false,
4934
+ generationProgress: 0,
4935
+ editNotes: [],
4936
+ editBars: 4,
4937
+ editBpm: 120,
4938
+ instrumentPluginId: handle.instrumentPluginId ?? null,
4939
+ instrumentName: handle.instrumentName ?? null,
4940
+ instrumentMissing: false,
4941
+ shuffleHistory: /* @__PURE__ */ new Set(),
4942
+ ...overrides
4943
+ };
4944
+ }
4945
+
4946
+ // src/panel-core/panel-helpers.ts
4947
+ function trackDataKey(dbId, suffix) {
4948
+ return `track:${dbId}:${suffix}`;
4949
+ }
4950
+ function pluginFxToToggleFx(sdkState) {
4951
+ const result = { ...EMPTY_FX_DETAIL_STATE };
4952
+ for (const category of ["eq", "compressor", "chorus", "phaser", "delay", "reverb"]) {
4953
+ const sdkCat = sdkState[category];
4954
+ if (sdkCat) {
4955
+ result[category] = {
4956
+ enabled: sdkCat.enabled,
4957
+ presetIndex: sdkCat.presetIndex,
4958
+ dryWet: sdkCat.dryWet
4959
+ };
4960
+ }
4961
+ }
4962
+ return result;
4963
+ }
4964
+ function parseLLMNoteResponse(content) {
4965
+ try {
4966
+ let jsonStr = content.trim();
4967
+ const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
4968
+ if (fenceMatch) {
4969
+ jsonStr = fenceMatch[1].trim();
4970
+ }
4971
+ const parsed = JSON.parse(jsonStr);
4972
+ if (typeof parsed !== "object" || parsed === null || !("notes" in parsed)) {
4973
+ return null;
4974
+ }
4975
+ const obj = parsed;
4976
+ if (!Array.isArray(obj.notes)) {
4977
+ return null;
4978
+ }
4979
+ const validNotes = [];
4980
+ for (const raw of obj.notes) {
4981
+ if (typeof raw !== "object" || raw === null) continue;
4982
+ const note = raw;
4983
+ const pitch = typeof note.pitch === "number" ? note.pitch : NaN;
4984
+ const startBeat = typeof note.startBeat === "number" ? note.startBeat : NaN;
4985
+ const durationBeats = typeof note.durationBeats === "number" ? note.durationBeats : NaN;
4986
+ const velocity = typeof note.velocity === "number" ? note.velocity : NaN;
4987
+ if (!isNaN(pitch) && pitch >= 0 && pitch <= 127 && !isNaN(startBeat) && startBeat >= 0 && !isNaN(durationBeats) && durationBeats > 0 && !isNaN(velocity) && velocity >= 1 && velocity <= 127) {
4988
+ validNotes.push({
4989
+ pitch: Math.round(pitch),
4990
+ startBeat,
4991
+ durationBeats,
4992
+ velocity: Math.round(velocity)
4993
+ });
4994
+ }
4995
+ }
4996
+ const role = typeof obj.role === "string" ? obj.role : void 0;
4997
+ return { notes: validNotes, role };
4998
+ } catch {
4999
+ return null;
5000
+ }
5001
+ }
5002
+
5003
+ // src/panel-core/group-meta.ts
5004
+ function parseTrackGroups(sceneData, spec) {
5005
+ const pattern = new RegExp(`^track:(.+):${spec.metaKey}$`);
5006
+ const groups = /* @__PURE__ */ new Map();
5007
+ for (const [key, val] of Object.entries(sceneData)) {
5008
+ const match = pattern.exec(key);
5009
+ if (!match) continue;
5010
+ const meta = spec.asMeta(val);
5011
+ if (!meta) continue;
5012
+ const groupId = spec.groupIdOf(meta);
5013
+ const list = groups.get(groupId) ?? [];
5014
+ list.push({ dbId: match[1], meta });
5015
+ groups.set(groupId, list);
5016
+ }
5017
+ const out = [];
5018
+ for (const [groupId, members] of groups) {
5019
+ if (spec.sortMembers) members.sort(spec.sortMembers);
5020
+ out.push({ groupId, members });
5021
+ }
5022
+ return out;
5023
+ }
5024
+ function resolveTrackGroups(parsedGroups, tracks, getDbId, opts = {}) {
5025
+ const byDbId = /* @__PURE__ */ new Map();
5026
+ for (const t of tracks) byDbId.set(getDbId(t), t);
5027
+ const resolved = [];
5028
+ const memberDbIds = /* @__PURE__ */ new Set();
5029
+ const staleMemberDbIds = [];
5030
+ for (const parsed of parsedGroups) {
5031
+ const live = { groupId: parsed.groupId, members: [] };
5032
+ for (const member of parsed.members) {
5033
+ const track = byDbId.get(member.dbId);
5034
+ if (track) live.members.push({ dbId: member.dbId, meta: member.meta, track });
5035
+ else staleMemberDbIds.push(member.dbId);
5036
+ }
5037
+ if (live.members.length === 0) continue;
5038
+ const complete = opts.isComplete ? opts.isComplete(live, parsed) : live.members.length === parsed.members.length;
5039
+ if (!complete) continue;
5040
+ resolved.push(live);
5041
+ for (const m of live.members) memberDbIds.add(m.dbId);
5042
+ }
5043
+ return { resolved, memberDbIds, staleMemberDbIds };
5044
+ }
5045
+
5046
+ // src/panel-core/useTransitionOps.ts
5047
+ import { useCallback as useCallback13, useEffect as useEffect15, useRef as useRef16, useState as useState18 } from "react";
5048
+ function useTransitionOps({
5049
+ host,
5050
+ adapter,
5051
+ activeSceneId,
5052
+ isConnected,
5053
+ isAuthenticated,
5054
+ sceneContext,
5055
+ tracks,
5056
+ setTracks,
5057
+ loadTracks,
5058
+ setCrossfadePairsMeta,
5059
+ setFadesMeta,
5060
+ resolvedCrossfadePairs,
5061
+ resolvedFades
5062
+ }) {
5063
+ const { identity } = adapter;
5064
+ const appliedFadeAutomationRef = useRef16(/* @__PURE__ */ new Set());
5065
+ const applyCrossfadeAutomation = useCallback13(
5066
+ async (originTrackId, targetTrackId, bars, bpm, sliderPos) => {
5067
+ if (host.setTrackVolumeAutomation) {
5068
+ const curves = buildCrossfadeVolumeCurves(bars, bpm, sliderPos);
5069
+ await host.setTrackVolumeAutomation(originTrackId, curves.origin).catch(() => {
5070
+ });
5071
+ await host.setTrackVolumeAutomation(targetTrackId, curves.target).catch(() => {
5072
+ });
5073
+ } else {
5074
+ await host.setTrackVolume(originTrackId, EQUAL_POWER_GAIN).catch(() => {
5075
+ });
5076
+ await host.setTrackVolume(targetTrackId, EQUAL_POWER_GAIN).catch(() => {
5077
+ });
5078
+ }
5079
+ },
5080
+ [host]
5081
+ );
5082
+ const applyFadeAutomation = useCallback13(
5083
+ async (trackId, direction, bars, bpm, sliderPos, gesture) => {
5084
+ if (!host.setTrackVolumeAutomation) return;
5085
+ const points = buildFadeVolumeCurve(bars, bpm, direction, sliderPos, gesture);
5086
+ await host.setTrackVolumeAutomation(trackId, points).catch(() => {
5087
+ });
5088
+ },
5089
+ [host]
5090
+ );
5091
+ const [isCreatingCrossfade, setIsCreatingCrossfade] = useState18(false);
5092
+ const handleCreateCrossfade = useCallback13(
5093
+ async (origin, target) => {
5094
+ const scene = activeSceneId;
5095
+ const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
5096
+ const toSceneId = sceneContext?.transitionToSceneId ?? "";
5097
+ if (!scene) throw new Error("No active scene.");
5098
+ if (!isConnected) throw new Error("Systems not connected.");
5099
+ if (!isAuthenticated) throw new Error("Please sign in to generate the bridge.");
5100
+ if (tracks.length + 2 > identity.maxTracks) {
5101
+ throw new Error("Not enough track slots for a crossfade.");
5102
+ }
5103
+ setIsCreatingCrossfade(true);
5104
+ const created = [];
5105
+ try {
5106
+ const role = target.role ?? origin.role ?? "";
5107
+ const mc = await host.getMusicalContext();
5108
+ const [originMidi, targetMidi, originKey, targetKey] = await Promise.all([
5109
+ host.readImportableTrackMidi ? host.readImportableTrackMidi(origin.dbId) : Promise.resolve({ clips: [] }),
5110
+ host.readImportableTrackMidi ? host.readImportableTrackMidi(target.dbId) : Promise.resolve({ clips: [] }),
5111
+ host.getSceneKey ? host.getSceneKey(fromSceneId) : Promise.resolve(null),
5112
+ host.getSceneKey ? host.getSceneKey(toSceneId) : Promise.resolve(null)
5113
+ ]);
5114
+ const userPrompt = buildCrossfadeInpaintPrompt({
5115
+ role,
5116
+ bars: mc.bars,
5117
+ originName: origin.name,
5118
+ targetName: target.name,
5119
+ originKey: originKey ? `${originKey.key} ${originKey.mode}` : null,
5120
+ targetKey: targetKey ? `${targetKey.key} ${targetKey.mode}` : null,
5121
+ originNotes: originMidi.clips[0]?.notes ?? [],
5122
+ targetNotes: targetMidi.clips[0]?.notes ?? []
5123
+ });
5124
+ const llm = await host.generateWithLLM({
5125
+ system: adapter.buildSystemPrompt(host.getValidRoles()),
5126
+ user: userPrompt,
5127
+ responseFormat: "json"
5128
+ });
5129
+ const parsed = adapter.parseNotesResponse(llm.content);
5130
+ if (!parsed || parsed.notes.length === 0) {
5131
+ throw new Error("The bridge generator returned no notes.");
5132
+ }
5133
+ const notes = await host.postProcessMidi(parsed.notes, {
5134
+ quantize: true,
5135
+ removeOverlaps: true
5136
+ });
5137
+ const clip = {
5138
+ startTime: 0,
5139
+ endTime: mc.bars * 4 * 60 / mc.bpm,
5140
+ tempo: mc.bpm,
5141
+ notes
5142
+ };
5143
+ const top = await host.createTrack({
5144
+ name: `${identity.trackNamePrefix}-${Date.now()}-xf-o`,
5145
+ ...adapter.createTrackOptions()
5146
+ });
5147
+ created.push(top);
5148
+ const bottom = await host.createTrack({
5149
+ name: `${identity.trackNamePrefix}-${Date.now()}-xf-t`,
5150
+ ...adapter.createTrackOptions()
5151
+ });
5152
+ created.push(bottom);
5153
+ if (role) {
5154
+ await host.setTrackRole(top.id, role).catch(() => {
5155
+ });
5156
+ await host.setTrackRole(bottom.id, role).catch(() => {
5157
+ });
5158
+ }
5159
+ await host.writeMidiClip(top.id, clip);
5160
+ await host.writeMidiClip(bottom.id, clip);
5161
+ const copySound = async (newTrackId, sourceDbId) => {
5162
+ if (!host.getTrackSound) return "default";
5163
+ const snap = await host.getTrackSound(sourceDbId);
5164
+ if (!snap || snap.kind !== adapter.sound.acceptedSnapshotKind) return "default";
5165
+ return adapter.sound.copySnapshot(newTrackId, snap);
5166
+ };
5167
+ const originLabel = await copySound(top.id, origin.dbId);
5168
+ const targetLabel = await copySound(bottom.id, target.dbId);
5169
+ await applyCrossfadeAutomation(top.id, bottom.id, mc.bars, mc.bpm, 0.5);
5170
+ const groupId = top.dbId;
5171
+ const originMeta = {
5172
+ groupId,
5173
+ slot: "origin",
5174
+ partnerDbId: bottom.dbId,
5175
+ sourceTrackDbId: origin.dbId,
5176
+ sourceSceneId: fromSceneId,
5177
+ sourceName: origin.name,
5178
+ soundLabel: originLabel,
5179
+ sliderPos: 0.5
5180
+ };
5181
+ const targetMeta = {
5182
+ groupId,
5183
+ slot: "target",
5184
+ partnerDbId: top.dbId,
5185
+ sourceTrackDbId: target.dbId,
5186
+ sourceSceneId: toSceneId,
5187
+ sourceName: target.name,
5188
+ soundLabel: targetLabel,
5189
+ sliderPos: 0.5
5190
+ };
5191
+ await host.setSceneData(scene, `track:${top.dbId}:crossfade`, originMeta);
5192
+ await host.setSceneData(scene, `track:${bottom.dbId}:crossfade`, targetMeta);
5193
+ await loadTracks(true);
5194
+ host.showToast("success", "Crossfade created", `${origin.name} \u2192 ${target.name}`);
5195
+ } catch (err) {
5196
+ for (const h of [...created].reverse()) {
5197
+ try {
5198
+ await host.deleteTrack(h.id);
5199
+ } catch {
5200
+ }
5201
+ }
5202
+ throw err instanceof Error ? err : new Error(String(err));
5203
+ } finally {
5204
+ setIsCreatingCrossfade(false);
5205
+ }
5206
+ },
5207
+ [
5208
+ host,
5209
+ adapter,
5210
+ identity,
5211
+ activeSceneId,
5212
+ isConnected,
5213
+ isAuthenticated,
5214
+ tracks.length,
5215
+ sceneContext,
5216
+ applyCrossfadeAutomation,
5217
+ loadTracks
5218
+ ]
5219
+ );
5220
+ const [isCreatingFade, setIsCreatingFade] = useState18(false);
5221
+ const handleCreateFade = useCallback13(
5222
+ async (selection, direction, gesture) => {
5223
+ const scene = activeSceneId;
5224
+ const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
5225
+ const toSceneId = sceneContext?.transitionToSceneId ?? "";
5226
+ if (!scene) throw new Error("No active scene.");
5227
+ if (!isConnected) throw new Error("Systems not connected.");
5228
+ if (!isAuthenticated) throw new Error("Please sign in to generate the fade.");
5229
+ if (tracks.length + 1 > identity.maxTracks) {
5230
+ throw new Error("Not enough track slots for a fade.");
5231
+ }
5232
+ setIsCreatingFade(true);
5233
+ const created = [];
5234
+ try {
5235
+ const role = selection.role ?? "";
5236
+ const sourceSceneId = direction === "out" ? fromSceneId : toSceneId;
5237
+ const mc = await host.getMusicalContext();
5238
+ const [srcMidi, srcKey] = await Promise.all([
5239
+ host.readImportableTrackMidi ? host.readImportableTrackMidi(selection.dbId) : Promise.resolve({ clips: [] }),
5240
+ host.getSceneKey ? host.getSceneKey(sourceSceneId) : Promise.resolve(null)
5241
+ ]);
5242
+ const srcNotes = srcMidi.clips[0]?.notes ?? [];
5243
+ const keyStr = srcKey ? `${srcKey.key} ${srcKey.mode}` : null;
5244
+ const userPrompt = buildCrossfadeInpaintPrompt({
5245
+ role,
5246
+ bars: mc.bars,
5247
+ originName: direction === "out" ? selection.name : "silence",
5248
+ targetName: direction === "in" ? selection.name : "silence",
5249
+ originKey: direction === "out" ? keyStr : null,
5250
+ targetKey: direction === "in" ? keyStr : null,
5251
+ originNotes: direction === "out" ? srcNotes : [],
5252
+ targetNotes: direction === "in" ? srcNotes : []
5253
+ });
5254
+ const llm = await host.generateWithLLM({
5255
+ system: adapter.buildSystemPrompt(host.getValidRoles()),
5256
+ user: userPrompt,
5257
+ responseFormat: "json"
5258
+ });
5259
+ const parsed = adapter.parseNotesResponse(llm.content);
5260
+ if (!parsed || parsed.notes.length === 0) {
5261
+ throw new Error("The fade generator returned no notes.");
5262
+ }
5263
+ const notes = await host.postProcessMidi(parsed.notes, {
5264
+ quantize: true,
5265
+ removeOverlaps: true
5266
+ });
5267
+ const clip = {
5268
+ startTime: 0,
5269
+ endTime: mc.bars * 4 * 60 / mc.bpm,
5270
+ tempo: mc.bpm,
5271
+ notes
5272
+ };
5273
+ const track = await host.createTrack({
5274
+ name: `${identity.trackNamePrefix}-${Date.now()}-fade-${direction}`,
5275
+ ...adapter.createTrackOptions()
5276
+ });
5277
+ created.push(track);
5278
+ if (role) await host.setTrackRole(track.id, role).catch(() => {
5279
+ });
5280
+ await host.writeMidiClip(track.id, clip);
5281
+ let soundLabel = "default";
5282
+ if (host.getTrackSound) {
5283
+ const snap = await host.getTrackSound(selection.dbId);
5284
+ if (snap && snap.kind === adapter.sound.acceptedSnapshotKind) {
5285
+ soundLabel = await adapter.sound.copySnapshot(track.id, snap);
5286
+ }
5287
+ }
5288
+ await applyFadeAutomation(track.id, direction, mc.bars, mc.bpm, 0.5, gesture);
5289
+ appliedFadeAutomationRef.current.add(track.id);
5290
+ const meta = {
5291
+ direction,
5292
+ gesture,
5293
+ sourceTrackDbId: selection.dbId,
5294
+ sourceSceneId,
5295
+ sourceName: selection.name,
5296
+ soundLabel,
5297
+ sliderPos: 0.5
5298
+ };
5299
+ await host.setSceneData(scene, `track:${track.dbId}:fade`, meta);
5300
+ await loadTracks(true);
5301
+ host.showToast(
5302
+ "success",
5303
+ direction === "in" ? "Fade in created" : "Fade out created",
5304
+ selection.name
5305
+ );
5306
+ } catch (err) {
5307
+ for (const h of [...created].reverse()) {
5308
+ try {
5309
+ await host.deleteTrack(h.id);
5310
+ } catch {
5311
+ }
5312
+ }
5313
+ throw err instanceof Error ? err : new Error(String(err));
5314
+ } finally {
5315
+ setIsCreatingFade(false);
5316
+ }
5317
+ },
5318
+ [
5319
+ host,
5320
+ adapter,
5321
+ identity,
5322
+ activeSceneId,
5323
+ isConnected,
5324
+ isAuthenticated,
5325
+ tracks.length,
5326
+ sceneContext,
5327
+ applyFadeAutomation,
5328
+ loadTracks
5329
+ ]
5330
+ );
5331
+ const handleCrossfadeMute = useCallback13(
5332
+ (pair) => {
5333
+ const newMuted = !pair.origin.runtimeState.muted;
5334
+ for (const id of [pair.origin.handle.id, pair.target.handle.id]) {
5335
+ setTracks(
5336
+ (prev) => prev.map(
5337
+ (t) => t.handle.id === id ? { ...t, runtimeState: { ...t.runtimeState, muted: newMuted } } : t
5338
+ )
5339
+ );
5340
+ host.setTrackMute(id, newMuted).catch(() => {
5341
+ });
5342
+ }
5343
+ },
5344
+ [host, setTracks]
5345
+ );
5346
+ const handleCrossfadeSolo = useCallback13(
5347
+ (pair) => {
5348
+ const newSolo = !pair.origin.runtimeState.solo;
5349
+ for (const id of [pair.origin.handle.id, pair.target.handle.id]) {
5350
+ setTracks(
5351
+ (prev) => prev.map(
5352
+ (t) => t.handle.id === id ? { ...t, runtimeState: { ...t.runtimeState, solo: newSolo } } : t
5353
+ )
5354
+ );
5355
+ host.setTrackSolo(id, newSolo).catch(() => {
5356
+ });
5357
+ }
5358
+ },
5359
+ [host, setTracks]
5360
+ );
5361
+ const handleCrossfadeDelete = useCallback13(
5362
+ async (pair) => {
5363
+ try {
5364
+ for (const member of [pair.origin, pair.target]) {
5365
+ await host.deleteTrack(member.handle.id);
5366
+ if (activeSceneId) {
5367
+ await host.deleteSceneData(activeSceneId, `track:${member.handle.dbId}:crossfade`);
5368
+ }
5369
+ }
5370
+ setCrossfadePairsMeta((prev) => prev.filter((p) => p.groupId !== pair.groupId));
5371
+ setTracks(
5372
+ (prev) => prev.filter(
5373
+ (t) => t.handle.id !== pair.origin.handle.id && t.handle.id !== pair.target.handle.id
5374
+ )
5375
+ );
5376
+ host.showToast("success", "Crossfade removed");
5377
+ } catch (err) {
5378
+ host.showToast(
5379
+ "error",
5380
+ "Failed to delete crossfade",
5381
+ err instanceof Error ? err.message : String(err)
5382
+ );
5383
+ }
5384
+ },
5385
+ [host, activeSceneId, setCrossfadePairsMeta, setTracks]
5386
+ );
5387
+ const crossfadeSliderTimers = useRef16({});
5388
+ const handleCrossfadeSlider = useCallback13(
5389
+ (pair, pos) => {
5390
+ setCrossfadePairsMeta(
5391
+ (prev) => prev.map((p) => p.groupId === pair.groupId ? { ...p, sliderPos: pos } : p)
5392
+ );
5393
+ if (crossfadeSliderTimers.current[pair.groupId]) {
5394
+ clearTimeout(crossfadeSliderTimers.current[pair.groupId]);
5395
+ }
5396
+ crossfadeSliderTimers.current[pair.groupId] = setTimeout(() => {
5397
+ void (async () => {
5398
+ const mc = await host.getMusicalContext();
5399
+ await applyCrossfadeAutomation(
5400
+ pair.origin.handle.id,
5401
+ pair.target.handle.id,
5402
+ mc.bars,
5403
+ mc.bpm,
5404
+ pos
5405
+ );
5406
+ if (activeSceneId) {
5407
+ const sceneData = await host.getAllSceneData(activeSceneId);
5408
+ for (const dbId of [pair.originDbId, pair.targetDbId]) {
5409
+ const meta = asCrossfadeMeta(sceneData[`track:${dbId}:crossfade`]);
5410
+ if (meta) {
5411
+ host.setSceneData(activeSceneId, `track:${dbId}:crossfade`, { ...meta, sliderPos: pos }).catch(() => {
5412
+ });
5413
+ }
5414
+ }
5415
+ }
5416
+ })();
5417
+ }, 200);
5418
+ },
5419
+ [host, activeSceneId, applyCrossfadeAutomation, setCrossfadePairsMeta]
5420
+ );
5421
+ const handleFadeDelete = useCallback13(
5422
+ async (fade) => {
5423
+ try {
5424
+ await host.deleteTrack(fade.track.handle.id);
5425
+ if (activeSceneId) {
5426
+ await host.deleteSceneData(activeSceneId, `track:${fade.dbId}:fade`);
5427
+ }
5428
+ setFadesMeta((prev) => prev.filter((f) => f.dbId !== fade.dbId));
5429
+ setTracks((prev) => prev.filter((t) => t.handle.id !== fade.track.handle.id));
5430
+ host.showToast("success", "Fade removed");
5431
+ } catch (err) {
5432
+ host.showToast(
5433
+ "error",
5434
+ "Failed to delete fade",
5435
+ err instanceof Error ? err.message : String(err)
5436
+ );
5437
+ }
5438
+ },
5439
+ [host, activeSceneId, setFadesMeta, setTracks]
5440
+ );
5441
+ const fadeSliderTimers = useRef16({});
5442
+ const handleFadeSlider = useCallback13(
5443
+ (fade, pos) => {
5444
+ setFadesMeta(
5445
+ (prev) => prev.map((f) => f.dbId === fade.dbId ? { ...f, meta: { ...f.meta, sliderPos: pos } } : f)
5446
+ );
5447
+ if (fadeSliderTimers.current[fade.dbId]) clearTimeout(fadeSliderTimers.current[fade.dbId]);
5448
+ fadeSliderTimers.current[fade.dbId] = setTimeout(() => {
5449
+ void (async () => {
5450
+ const mc = await host.getMusicalContext();
5451
+ await applyFadeAutomation(
5452
+ fade.track.handle.id,
5453
+ fade.meta.direction,
5454
+ mc.bars,
5455
+ mc.bpm,
5456
+ pos,
5457
+ fade.meta.gesture
5458
+ );
5459
+ if (activeSceneId) {
5460
+ const sceneData = await host.getAllSceneData(activeSceneId);
5461
+ const meta = asFadeMeta(sceneData[`track:${fade.dbId}:fade`]);
5462
+ if (meta) {
5463
+ host.setSceneData(activeSceneId, `track:${fade.dbId}:fade`, { ...meta, sliderPos: pos }).catch(() => {
5464
+ });
5465
+ }
5466
+ }
5467
+ })();
5468
+ }, 200);
5469
+ },
5470
+ [host, activeSceneId, applyFadeAutomation, setFadesMeta]
5471
+ );
5472
+ const lastResyncKeyRef = useRef16("");
5473
+ useEffect15(() => {
5474
+ if (!host.getTrackSound || resolvedCrossfadePairs.length === 0 && resolvedFades.length === 0) {
5475
+ return;
5476
+ }
5477
+ const resyncKey = [
5478
+ ...resolvedCrossfadePairs.map(
5479
+ (p) => `${p.origin.handle.dbId}<${p.originSourceDbId}|${p.target.handle.dbId}<${p.targetSourceDbId}`
5480
+ ),
5481
+ ...resolvedFades.map((f) => `${f.track.handle.dbId}<${f.meta.sourceTrackDbId}`)
5482
+ ].join(",");
5483
+ if (resyncKey === lastResyncKeyRef.current) return;
5484
+ lastResyncKeyRef.current = resyncKey;
5485
+ let cancelled = false;
5486
+ const reapplyIfDrifted = async (layerTrackId, layerDbId, sourceDbId) => {
5487
+ if (!host.getTrackSound || cancelled) return;
5488
+ const [sourceSnap, layerSnap] = await Promise.all([
5489
+ host.getTrackSound(sourceDbId),
5490
+ host.getTrackSound(layerDbId)
5491
+ ]);
5492
+ if (cancelled || !sourceSnap || sourceSnap.kind !== adapter.sound.acceptedSnapshotKind) {
5493
+ return;
5494
+ }
5495
+ if (soundIdentity(sourceSnap) === soundIdentity(layerSnap)) return;
5496
+ try {
5497
+ await adapter.sound.copySnapshot(layerTrackId, sourceSnap);
5498
+ } catch {
5499
+ }
5500
+ };
5501
+ void (async () => {
5502
+ for (const pair of resolvedCrossfadePairs) {
5503
+ await reapplyIfDrifted(pair.origin.handle.id, pair.origin.handle.dbId, pair.originSourceDbId);
5504
+ await reapplyIfDrifted(pair.target.handle.id, pair.target.handle.dbId, pair.targetSourceDbId);
5505
+ }
5506
+ for (const fade of resolvedFades) {
5507
+ await reapplyIfDrifted(fade.track.handle.id, fade.track.handle.dbId, fade.meta.sourceTrackDbId);
5508
+ }
5509
+ })();
5510
+ return () => {
5511
+ cancelled = true;
5512
+ };
5513
+ }, [resolvedCrossfadePairs, resolvedFades, host, adapter]);
5514
+ useEffect15(() => {
5515
+ if (!host.setTrackVolumeAutomation || resolvedFades.length === 0) return;
5516
+ void (async () => {
5517
+ const mc = await host.getMusicalContext();
5518
+ for (const fade of resolvedFades) {
5519
+ const id = fade.track.handle.id;
5520
+ if (appliedFadeAutomationRef.current.has(id)) continue;
5521
+ appliedFadeAutomationRef.current.add(id);
5522
+ await applyFadeAutomation(
5523
+ id,
5524
+ fade.meta.direction,
5525
+ mc.bars,
5526
+ mc.bpm,
5527
+ fade.meta.sliderPos,
5528
+ fade.meta.gesture
5529
+ );
5530
+ }
5531
+ })();
5532
+ }, [resolvedFades, host, applyFadeAutomation]);
5533
+ return {
5534
+ isCreatingCrossfade,
5535
+ isCreatingFade,
5536
+ handleCreateCrossfade,
5537
+ handleCreateFade,
5538
+ handleCrossfadeMute,
5539
+ handleCrossfadeSolo,
5540
+ handleCrossfadeDelete,
5541
+ handleCrossfadeSlider,
5542
+ handleFadeDelete,
5543
+ handleFadeSlider
5544
+ };
5545
+ }
5546
+
5547
+ // src/panel-core/useGeneratorPanelCore.tsx
5548
+ import { jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
5549
+ var EMPTY_PLACEHOLDERS = [];
5550
+ function useGeneratorPanelCore({
5551
+ ui,
5552
+ adapter
5553
+ }) {
5554
+ const {
5555
+ host,
5556
+ activeSceneId,
5557
+ isAuthenticated,
5558
+ isConnected,
5559
+ onHeaderContent,
5560
+ onLoading,
5561
+ sceneContext,
5562
+ onOpenContract,
5563
+ onExpandSelf,
5564
+ isExpanded
5565
+ } = ui;
5566
+ const { identity, features } = adapter;
5567
+ const logTag = identity.logTag;
5568
+ const adapterRef = useRef17(adapter);
5569
+ useEffect16(() => {
5570
+ if (adapterRef.current !== adapter) {
5571
+ adapterRef.current = adapter;
5572
+ console.warn(
5573
+ `[${logTag}] GeneratorPanelAdapter identity changed between renders \u2014 wrap it in useMemo(() => createAdapter(host), [host]) to avoid load loops.`
5574
+ );
5575
+ }
5576
+ }, [adapter, logTag]);
5577
+ const supportsMeters = typeof host.getTrackLevels === "function";
5578
+ const trackLevels = useTrackLevels(host, isExpanded);
5579
+ const [tracks, setTracks] = useState19([]);
5580
+ const [isLoadingTracks, setIsLoadingTracks] = useState19(false);
5581
+ const [importOpen, setImportOpen] = useState19(false);
5582
+ const [soundImportTarget, setSoundImportTarget] = useState19(null);
5583
+ const [designerView, setDesignerView] = useState19(false);
5584
+ const [transitionSourceTotal, setTransitionSourceTotal] = useState19(0);
5585
+ const [crossfadePairsMeta, setCrossfadePairsMeta] = useState19([]);
5586
+ const [fadesMeta, setFadesMeta] = useState19([]);
5587
+ const [genericGroupMetas, setGenericGroupMetas] = useState19({});
5588
+ const [isComposing, , setIsComposingForScene] = useSceneState(activeSceneId, false);
5589
+ const [placeholders, , setPlaceholdersForScene] = useSceneState(
5590
+ activeSceneId,
5591
+ EMPTY_PLACEHOLDERS
5592
+ );
5593
+ const saveTimeoutRefs = useRef17({});
5594
+ const editLoadStartedRef = useRef17(/* @__PURE__ */ new Set());
5595
+ const [availableInstruments, setAvailableInstruments] = useState19([]);
5596
+ const [instrumentsLoading, setInstrumentsLoading] = useState19(false);
5597
+ const engineToDbIdRef = useRef17(/* @__PURE__ */ new Map());
5598
+ const tracksLoadedForSceneRef = useRef17(null);
5599
+ const persistSoundHistory = useCallback14(
5600
+ (trackId, state) => {
5601
+ if (!activeSceneId) return;
5602
+ const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
5603
+ host.setSceneData(activeSceneId, trackDataKey(dbId, "soundHistory"), state).catch(() => {
5604
+ });
5605
+ },
5606
+ [host, activeSceneId]
5607
+ );
5608
+ const soundHistory = useSoundHistory(adapter.sound.applySound, {
5609
+ max: adapter.sound.historyMax,
5610
+ onChange: persistSoundHistory
5611
+ });
5612
+ const anySolo = useAnySolo(host);
5613
+ const reorder = useTrackReorder({
5614
+ host,
5615
+ items: tracks,
5616
+ setItems: setTracks,
5617
+ getId: (t) => t.handle.dbId
5618
+ });
5619
+ const loadTracks = useCallback14(
5620
+ async (incremental = false) => {
5621
+ const sceneAtStart = activeSceneId;
5622
+ if (!sceneAtStart) {
5623
+ setTracks([]);
5624
+ setCrossfadePairsMeta([]);
5625
+ setFadesMeta([]);
5626
+ setGenericGroupMetas({});
5627
+ tracksLoadedForSceneRef.current = null;
5628
+ setIsLoadingTracks(false);
5629
+ return;
5630
+ }
5631
+ if (!incremental && tracksLoadedForSceneRef.current !== sceneAtStart) {
5632
+ setTracks([]);
5633
+ }
5634
+ tracksLoadedForSceneRef.current = sceneAtStart;
5635
+ if (!incremental) soundHistory.reset();
5636
+ const isStale = () => tracksLoadedForSceneRef.current !== sceneAtStart;
5637
+ if (!incremental) setIsLoadingTracks(true);
5638
+ try {
5639
+ await host.adoptSceneTracks();
5640
+ if (isStale()) return;
5641
+ const handles = await host.getPluginTracks();
5642
+ if (isStale()) return;
5643
+ const sceneData = await host.getAllSceneData(sceneAtStart);
5644
+ if (isStale()) return;
5645
+ const idMap = /* @__PURE__ */ new Map();
5646
+ for (const h of handles) {
5647
+ idMap.set(h.id, h.dbId);
5648
+ }
5649
+ engineToDbIdRef.current = idMap;
5650
+ const trackStates = [];
5651
+ for (const handle of handles) {
5652
+ let runtimeState = {
5653
+ id: handle.id,
5654
+ muted: false,
5655
+ solo: false,
5656
+ volume: 0.75,
5657
+ pan: 0
5658
+ };
5659
+ let hasMidi = false;
5660
+ try {
5661
+ const info = await host.getTrackInfo(handle.id);
5662
+ runtimeState = {
5663
+ id: handle.id,
5664
+ muted: info.muted,
5665
+ solo: info.soloed,
5666
+ volume: info.volume,
5667
+ pan: info.pan
5668
+ };
5669
+ hasMidi = info.hasMidi;
5670
+ } catch {
5671
+ }
5672
+ let fxDetailState = newTrackState(handle).fxDetailState;
5673
+ try {
5674
+ const fxState = await host.getTrackFxState(handle.id);
5675
+ fxDetailState = pluginFxToToggleFx(fxState);
5676
+ } catch {
5677
+ }
5678
+ const promptKey = trackDataKey(handle.dbId, "prompt");
5679
+ let prompt = typeof sceneData[promptKey] === "string" ? sceneData[promptKey] : "";
5680
+ if (!prompt && handle.prompt) {
5681
+ prompt = handle.prompt;
5682
+ host.setSceneData(sceneAtStart, promptKey, prompt).catch(() => {
5683
+ });
5684
+ }
5685
+ if (!hasMidi && handle.role) {
5686
+ hasMidi = true;
5687
+ }
5688
+ let instrumentMissing = false;
5689
+ if (handle.instrumentPluginId) {
5690
+ try {
5691
+ const instrDescriptor = await host.getTrackInstrument(handle.id);
5692
+ if (instrDescriptor?.missing) {
5693
+ instrumentMissing = true;
5694
+ }
5695
+ } catch {
5696
+ }
5697
+ }
5698
+ trackStates.push(
5699
+ newTrackState(handle, {
5700
+ prompt,
5701
+ role: handle.role ?? "",
5702
+ runtimeState,
5703
+ fxDetailState,
5704
+ hasMidi,
5705
+ instrumentMissing
5706
+ })
5707
+ );
5708
+ }
5709
+ if (isStale()) return;
5710
+ setTracks((prev) => {
5711
+ const prevByDbId = new Map(prev.map((p) => [p.handle.dbId, p]));
5712
+ return trackStates.map((ts) => {
5713
+ const carry = prevByDbId.get(ts.handle.dbId);
5714
+ return carry ? { ...ts, editNotes: carry.editNotes, editBars: carry.editBars, editBpm: carry.editBpm } : ts;
5715
+ });
5716
+ });
5717
+ for (const ts of trackStates) {
5718
+ const persisted = sceneData[trackDataKey(ts.handle.dbId, "soundHistory")];
5719
+ if (persisted && typeof persisted === "object") {
5720
+ soundHistory.restore(ts.handle.id, persisted);
5721
+ }
5722
+ }
5723
+ if (!isStale()) {
5724
+ setCrossfadePairsMeta(parseCrossfadePairs(sceneData));
5725
+ setFadesMeta(parseFades(sceneData));
5726
+ if (adapter.groupExtensions && adapter.groupExtensions.length > 0) {
5727
+ const map = {};
5728
+ for (const ext of adapter.groupExtensions) {
5729
+ map[ext.metaKey] = parseTrackGroups(sceneData, ext);
5730
+ }
5731
+ setGenericGroupMetas(map);
5732
+ }
5733
+ }
5734
+ } catch (error) {
5735
+ console.error(`[${logTag}] Failed to load tracks:`, error);
5736
+ } finally {
5737
+ if (tracksLoadedForSceneRef.current === sceneAtStart) {
5738
+ setIsLoadingTracks(false);
5739
+ }
5740
+ }
5741
+ },
5742
+ [host, activeSceneId, soundHistory, adapter, logTag]
5743
+ );
5744
+ useEffect16(() => {
5745
+ loadTracks();
5746
+ }, [loadTracks]);
5747
+ useEffect16(() => {
5748
+ const map = /* @__PURE__ */ new Map();
5749
+ for (const t of tracks) {
5750
+ map.set(t.handle.id, t.handle.dbId);
5751
+ }
5752
+ engineToDbIdRef.current = map;
5753
+ }, [tracks]);
5754
+ const loadedCompletedIdsRef = useRef17(/* @__PURE__ */ new Set());
5755
+ useEffect16(() => {
5756
+ if (placeholders.length === 0) {
5757
+ loadedCompletedIdsRef.current.clear();
5758
+ return;
5759
+ }
5760
+ const newCompleted = placeholders.filter(
5761
+ (ph) => ph.status === "completed" && !loadedCompletedIdsRef.current.has(ph.id)
5762
+ );
5763
+ if (newCompleted.length > 0) {
5764
+ for (const ph of newCompleted) {
5765
+ loadedCompletedIdsRef.current.add(ph.id);
5766
+ }
5767
+ console.log(
5768
+ `[${logTag}] ${newCompleted.length} track(s) completed, reloading. IDs:`,
5769
+ newCompleted.map((ph) => ph.id)
5770
+ );
5771
+ loadTracks(true);
5772
+ }
5773
+ }, [placeholders, loadTracks, logTag]);
5774
+ const adoptAndLoad = useCallback14(() => {
5775
+ loadTracks(true);
5776
+ }, [loadTracks]);
5777
+ useEffect16(() => {
5778
+ const unsub = host.onEngineReady(() => {
5779
+ adoptAndLoad();
5780
+ });
5781
+ return unsub;
5782
+ }, [host, adoptAndLoad]);
5783
+ useEffect16(() => {
5784
+ if (typeof host.onAfterAgentMutation !== "function") return;
5785
+ let timer = null;
5786
+ const unsub = host.onAfterAgentMutation(() => {
5787
+ if (timer) clearTimeout(timer);
5788
+ timer = setTimeout(() => {
5789
+ timer = null;
5790
+ loadTracks(true);
5791
+ }, 500);
5792
+ });
5793
+ return () => {
5794
+ unsub?.();
5795
+ if (timer) clearTimeout(timer);
5796
+ };
5797
+ }, [host, loadTracks]);
5798
+ useEffect16(() => {
5799
+ const unsub = host.onTrackStateChange((trackId, state) => {
5800
+ setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: state } : t));
5801
+ });
5802
+ return unsub;
5803
+ }, [host]);
5804
+ useEffect16(() => {
5805
+ if (!features.bulkComposePlaceholders) return;
5806
+ console.log(`[${logTag}] Subscribing to composeProgress`);
5807
+ const unsub = host.onComposeProgress((event) => {
5808
+ const targetScene = event.sceneId;
5809
+ if (!targetScene) return;
5810
+ console.log(
5811
+ `[${logTag}] composeProgress event:`,
5812
+ event.phase,
5813
+ "sceneId:",
5814
+ targetScene,
5815
+ "placeholders:",
5816
+ event.placeholders?.length ?? "none"
5817
+ );
5818
+ switch (event.phase) {
5819
+ case "planning":
5820
+ setIsComposingForScene(targetScene, true);
5821
+ setPlaceholdersForScene(targetScene, []);
5822
+ break;
5823
+ case "generating":
5824
+ setIsComposingForScene(targetScene, false);
5825
+ if (event.placeholders) {
5826
+ setPlaceholdersForScene(targetScene, event.placeholders);
5827
+ }
5828
+ break;
5829
+ case "complete":
5830
+ case "error":
5831
+ setIsComposingForScene(targetScene, false);
5832
+ setPlaceholdersForScene(targetScene, EMPTY_PLACEHOLDERS);
5833
+ break;
5834
+ }
5835
+ });
5836
+ return unsub;
5837
+ }, [host, setIsComposingForScene, setPlaceholdersForScene, features.bulkComposePlaceholders, logTag]);
5838
+ useEffect16(() => {
5839
+ const refs = saveTimeoutRefs;
5840
+ return () => {
5841
+ for (const timeout of Object.values(refs.current)) {
5842
+ clearTimeout(timeout);
5843
+ }
5844
+ };
5845
+ }, []);
5846
+ const isAddingTrackRef = useRef17(false);
5847
+ const [isAddingTrack, setIsAddingTrack] = useState19(false);
5848
+ const handleAddTrack = useCallback14(async () => {
5849
+ if (isAddingTrackRef.current) return;
5850
+ if (!activeSceneId) {
5851
+ host.showToast("warning", "Select SCENE");
5852
+ return;
5853
+ }
5854
+ if (!isConnected) {
5855
+ host.showToast("warning", "Systems not connected");
5856
+ return;
5857
+ }
5858
+ if (!isAuthenticated) {
5859
+ host.showToast("warning", "Sign In Required", "Please sign in to add tracks");
5860
+ return;
5861
+ }
5862
+ if (tracks.length >= identity.maxTracks) return;
5863
+ isAddingTrackRef.current = true;
5864
+ setIsAddingTrack(true);
5865
+ try {
5866
+ const handle = await host.createTrack({
5867
+ name: `${identity.trackNamePrefix}-${Date.now()}`,
5868
+ ...adapter.createTrackOptions()
5869
+ });
5870
+ setTracks((prev) => [...prev, newTrackState(handle)]);
5871
+ onExpandSelf?.();
5872
+ setTimeout(() => {
5873
+ const inputs = document.querySelectorAll(
5874
+ `[data-testid="${identity.familyKey}-section"] [data-testid="sdk-prompt-input"]`
5875
+ );
5876
+ if (inputs.length > 0) {
5877
+ inputs[inputs.length - 1].focus();
5878
+ }
5879
+ }, 350);
5880
+ } catch (error) {
5881
+ const msg = error instanceof Error ? error.message : "Unknown error";
5882
+ host.showToast("error", "Failed to create track", msg);
5883
+ } finally {
5884
+ isAddingTrackRef.current = false;
5885
+ setIsAddingTrack(false);
5886
+ }
5887
+ }, [host, adapter, identity, activeSceneId, isConnected, isAuthenticated, tracks.length, onExpandSelf]);
5888
+ const handlePortTrack = useCallback14(
5889
+ async (sel) => {
5890
+ if (!activeSceneId) {
5891
+ host.showToast("warning", "Select SCENE");
5892
+ return;
5893
+ }
5894
+ if (!isConnected) {
5895
+ host.showToast("warning", "Systems not connected");
5896
+ return;
5897
+ }
5898
+ if (tracks.length >= identity.maxTracks) {
5899
+ host.showToast("warning", "Track limit reached");
5900
+ return;
5901
+ }
5902
+ if (!host.readImportableTrackMidi) return;
5903
+ let handle = null;
5904
+ try {
5905
+ handle = await host.createTrack({
5906
+ name: `${identity.trackNamePrefix}-${Date.now()}`,
5907
+ ...adapter.createTrackOptions()
5908
+ });
5909
+ if (sel.role) {
5910
+ try {
5911
+ await host.setTrackRole(handle.id, sel.role);
5912
+ } catch {
5913
+ }
5914
+ }
5915
+ const midi = await host.readImportableTrackMidi(sel.sourceTrackDbId);
5916
+ const notes = midi.clips[0]?.notes ?? [];
5917
+ if (notes.length > 0) {
5918
+ const mc = await host.getMusicalContext();
5919
+ await host.writeMidiClip(handle.id, {
5920
+ startTime: 0,
5921
+ endTime: mc.bars * 4 * 60 / mc.bpm,
5922
+ tempo: mc.bpm,
5923
+ notes
5924
+ });
5925
+ }
5926
+ await adapter.applyPortedTrackSound(handle, sel.role);
5927
+ host.showToast(
5928
+ "success",
5929
+ `Imported to ${identity.familyKey}`,
5930
+ notes.length ? `${sel.trackName} \u2192 ${identity.familyKey}` : `${sel.trackName} (no MIDI yet)`
5931
+ );
5932
+ await loadTracks(true);
5933
+ } catch (err) {
5934
+ if (handle) {
5935
+ try {
5936
+ await host.deleteTrack(handle.id);
5937
+ } catch {
5938
+ }
5939
+ }
5940
+ host.showToast("error", "Import failed", err instanceof Error ? err.message : String(err));
5941
+ }
5942
+ },
5943
+ [host, adapter, identity, activeSceneId, isConnected, tracks.length, loadTracks]
5944
+ );
5945
+ const handleSoundImportPick = useCallback14(
5946
+ async (sel) => {
5947
+ const target = soundImportTarget;
5948
+ if (!target || !host.getTrackSound) {
5949
+ setSoundImportTarget(null);
5950
+ return;
5951
+ }
5952
+ const noun = adapter.sound.importNoun;
5953
+ const nounTitle = noun.charAt(0).toUpperCase() + noun.slice(1);
5954
+ try {
5955
+ const snap = await host.getTrackSound(sel.sourceTrackDbId);
5956
+ if (!snap || snap.kind !== adapter.sound.acceptedSnapshotKind) {
5957
+ host.showToast(
5958
+ "error",
5959
+ `No ${noun} to import`,
5960
+ `${sel.trackName} has no ${identity.familyKey} ${noun}.`
5961
+ );
5962
+ return;
5963
+ }
5964
+ const descriptor = adapter.sound.descriptorFromSnapshot(snap);
5965
+ await adapter.sound.applySound(target.handle.id, descriptor);
5966
+ soundHistory.record(target.handle.id, descriptor, snap.label);
5967
+ host.showToast("success", `${nounTitle} imported`, `${snap.label} \u2192 ${target.handle.name}`);
5968
+ } catch (err) {
5969
+ host.showToast("error", "Import failed", err instanceof Error ? err.message : String(err));
5970
+ } finally {
5971
+ setSoundImportTarget(null);
5972
+ }
5973
+ },
5974
+ [soundImportTarget, host, adapter, identity.familyKey, soundHistory]
5975
+ );
5976
+ const [isExportingMidi, setIsExportingMidi] = useState19(false);
5977
+ const handleExportMidi = useCallback14(async () => {
5978
+ if (isExportingMidi) return;
5979
+ setIsExportingMidi(true);
5980
+ try {
5981
+ const result = await host.exportTracksAsMidiBundle({
5982
+ defaultName: identity.exportDefaultName ?? "midi-tracks"
5983
+ });
5984
+ if (result.success) {
5985
+ const filename = result.filePath.split("/").pop() || result.filePath;
5986
+ const skippedNote = result.skippedCount > 0 ? ` (${result.skippedCount} empty track${result.skippedCount === 1 ? "" : "s"} skipped)` : "";
5987
+ host.showToast(
5988
+ "success",
5989
+ "MIDI exported",
5990
+ `${result.trackCount} track${result.trackCount === 1 ? "" : "s"} \u2192 ${filename}${skippedNote}`
5991
+ );
5992
+ } else if (!("canceled" in result && result.canceled)) {
5993
+ const errMsg = "error" in result ? result.error : "Unknown error";
5994
+ host.showToast("error", "Export failed", errMsg);
5995
+ }
5996
+ } catch (error) {
5997
+ const msg = error instanceof Error ? error.message : String(error);
5998
+ host.showToast("error", "Export failed", msg);
5999
+ } finally {
6000
+ setIsExportingMidi(false);
6001
+ }
6002
+ }, [host, identity.exportDefaultName, isExportingMidi]);
6003
+ const isBulkActive = !!(isComposing || placeholders.length > 0);
6004
+ const needsContract = !sceneContext?.hasContract;
6005
+ const xfFromId = sceneContext?.transitionFromSceneId ?? null;
6006
+ const xfToId = sceneContext?.transitionToSceneId ?? null;
6007
+ const canCrossfade = features.transitionDesigner && sceneContext?.sceneType === "transition" && !!xfFromId && !!xfToId && !!host.listSceneFamilyTracks;
6008
+ useEffect16(() => {
6009
+ if (!canCrossfade) setDesignerView(false);
6010
+ }, [canCrossfade]);
6011
+ useEffect16(() => {
6012
+ if (!canCrossfade || !xfFromId || !xfToId || !host.listSceneFamilyTracks) {
6013
+ setTransitionSourceTotal(0);
6014
+ return;
6015
+ }
6016
+ let cancelled = false;
6017
+ void Promise.all([host.listSceneFamilyTracks(xfFromId), host.listSceneFamilyTracks(xfToId)]).then(([a, b]) => {
6018
+ if (!cancelled) setTransitionSourceTotal(a.length + b.length);
6019
+ }).catch(() => {
6020
+ if (!cancelled) setTransitionSourceTotal(0);
6021
+ });
6022
+ return () => {
6023
+ cancelled = true;
6024
+ };
6025
+ }, [canCrossfade, xfFromId, xfToId, host]);
6026
+ const transitionDone = crossfadePairsMeta.length * 2 + fadesMeta.length;
6027
+ useEffect16(() => {
6028
+ if (!onHeaderContent) return;
6029
+ const addDisabled = needsContract || !isConnected || !activeSceneId || tracks.length >= identity.maxTracks || isAddingTrack;
6030
+ onHeaderContent(
6031
+ /* @__PURE__ */ jsxs17("div", { className: "flex gap-1 items-center", children: [
6032
+ features.importTracks && (!canCrossfade || !designerView) && host.listImportableTracks && /* @__PURE__ */ jsx23(
6033
+ "button",
6034
+ {
6035
+ "data-testid": `import-from-scene-${identity.familyKey}-button`,
6036
+ onClick: (e) => {
6037
+ e.stopPropagation();
6038
+ onExpandSelf?.();
6039
+ setImportOpen(true);
6040
+ },
6041
+ disabled: !activeSceneId || needsContract,
6042
+ className: `px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors ${!activeSceneId || needsContract ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : "bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
6043
+ children: identity.importTrackLabel ?? "Import Track"
6044
+ }
6045
+ ),
6046
+ (!canCrossfade || !designerView) && /* @__PURE__ */ jsx23(
6047
+ "button",
6048
+ {
6049
+ "data-testid": `add-${identity.familyKey}-track-button`,
6050
+ onClick: (e) => {
6051
+ e.stopPropagation();
6052
+ if (needsContract) {
6053
+ onOpenContract?.();
6054
+ return;
6055
+ }
6056
+ handleAddTrack();
6057
+ },
6058
+ className: `px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors ${addDisabled ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : "bg-sas-accent/10 border-sas-accent/30 text-sas-accent hover:bg-sas-accent/20"}`,
6059
+ children: identity.addTrackLabel ?? "Add Track"
6060
+ }
6061
+ ),
6062
+ canCrossfade && /* @__PURE__ */ jsxs17(
6063
+ "button",
6064
+ {
6065
+ "data-testid": `${identity.familyKey}-view-toggle`,
6066
+ onClick: (e) => {
6067
+ e.stopPropagation();
6068
+ if (!designerView) {
6069
+ if (needsContract) {
6070
+ onOpenContract?.();
6071
+ return;
6072
+ }
6073
+ onExpandSelf?.();
6074
+ }
6075
+ setDesignerView((v) => !v);
6076
+ },
6077
+ disabled: !designerView && needsContract,
6078
+ title: designerView ? "Back to the track list" : "Open the transition designer",
6079
+ className: "relative overflow-hidden px-2 py-0.5 text-[10px] font-medium rounded-sm border border-sas-accent/40 text-sas-accent transition-colors hover:border-sas-accent disabled:opacity-50",
6080
+ children: [
6081
+ transitionSourceTotal > 0 && /* @__PURE__ */ jsx23(
6082
+ "span",
6083
+ {
6084
+ className: "absolute inset-y-0 left-0 bg-sas-accent/25",
6085
+ style: { width: `${Math.min(100, transitionDone / transitionSourceTotal * 100)}%` },
6086
+ "aria-hidden": true
6087
+ }
6088
+ ),
6089
+ /* @__PURE__ */ jsxs17("span", { className: "relative", children: [
6090
+ "\u21C4 ",
6091
+ designerView ? "Transition" : "Tracks",
6092
+ transitionSourceTotal > 0 ? ` ${transitionDone}/${transitionSourceTotal}` : ""
6093
+ ] })
6094
+ ]
6095
+ }
6096
+ )
6097
+ ] })
6098
+ );
6099
+ return () => {
6100
+ onHeaderContent(null);
6101
+ };
6102
+ }, [
6103
+ onHeaderContent,
6104
+ needsContract,
6105
+ isConnected,
6106
+ activeSceneId,
6107
+ tracks.length,
6108
+ isAddingTrack,
6109
+ handleAddTrack,
6110
+ onOpenContract,
6111
+ host,
6112
+ canCrossfade,
6113
+ designerView,
6114
+ transitionDone,
6115
+ transitionSourceTotal,
6116
+ onExpandSelf,
6117
+ identity,
6118
+ features.importTracks
6119
+ ]);
6120
+ useEffect16(() => {
6121
+ if (!onLoading) return;
6122
+ const anyGenerating = tracks.some((t) => t.isGenerating);
6123
+ onLoading(isLoadingTracks || anyGenerating || isBulkActive);
6124
+ return () => {
6125
+ onLoading(false);
6126
+ };
6127
+ }, [onLoading, isLoadingTracks, tracks, isBulkActive]);
6128
+ const handleDeleteTrack = useCallback14(
6129
+ async (trackId) => {
6130
+ try {
6131
+ await host.deleteTrack(trackId);
6132
+ const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
6133
+ if (activeSceneId) {
6134
+ await host.deleteSceneData(activeSceneId, trackDataKey(dbId, "prompt"));
6135
+ }
6136
+ setTracks((prev) => prev.filter((t) => t.handle.id !== trackId));
6137
+ } catch (error) {
6138
+ const msg = error instanceof Error ? error.message : "Unknown error";
6139
+ host.showToast("error", "Failed to delete track", msg);
6140
+ }
6141
+ },
6142
+ [host, activeSceneId]
6143
+ );
6144
+ const handlePromptChange = useCallback14(
6145
+ (trackId, prompt) => {
6146
+ setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, prompt } : t));
6147
+ const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
6148
+ if (saveTimeoutRefs.current[trackId]) {
6149
+ clearTimeout(saveTimeoutRefs.current[trackId]);
6150
+ }
6151
+ saveTimeoutRefs.current[trackId] = setTimeout(() => {
6152
+ if (activeSceneId) {
6153
+ host.setSceneData(activeSceneId, trackDataKey(dbId, "prompt"), prompt).catch(() => {
6154
+ });
6155
+ }
6156
+ }, 500);
6157
+ },
6158
+ [host, activeSceneId]
6159
+ );
6160
+ const resolvedGenericGroups = useMemo8(() => {
6161
+ const out = {};
6162
+ for (const ext of adapter.groupExtensions ?? []) {
6163
+ out[ext.metaKey] = resolveTrackGroups(
6164
+ genericGroupMetas[ext.metaKey] ?? [],
6165
+ tracks,
6166
+ (t) => t.handle.dbId,
6167
+ {
6168
+ isComplete: ext.isComplete
6169
+ }
6170
+ );
6171
+ }
6172
+ return out;
6173
+ }, [adapter, genericGroupMetas, tracks]);
6174
+ const genericGroupMemberDbIds = useMemo8(() => {
6175
+ const s = /* @__PURE__ */ new Set();
6176
+ for (const r of Object.values(resolvedGenericGroups)) {
6177
+ for (const dbId of r.memberDbIds) s.add(dbId);
6178
+ }
6179
+ return s;
6180
+ }, [resolvedGenericGroups]);
6181
+ const engineToDbId = useCallback14(
6182
+ (trackId) => engineToDbIdRef.current.get(trackId) ?? trackId,
6183
+ []
6184
+ );
6185
+ const updateTrack = useCallback14(
6186
+ (trackId, patch) => {
6187
+ setTracks(
6188
+ (prev) => prev.map(
6189
+ (t) => t.handle.id === trackId ? typeof patch === "function" ? patch(t) : { ...t, ...patch } : t
6190
+ )
6191
+ );
6192
+ },
6193
+ []
6194
+ );
6195
+ const markEditLoaded = useCallback14((trackId) => {
6196
+ editLoadStartedRef.current.add(trackId);
6197
+ }, []);
6198
+ const tracksRef = useRef17(tracks);
6199
+ useEffect16(() => {
6200
+ tracksRef.current = tracks;
6201
+ }, [tracks]);
6202
+ const resolvedGenericGroupsRef = useRef17(resolvedGenericGroups);
6203
+ useEffect16(() => {
6204
+ resolvedGenericGroupsRef.current = resolvedGenericGroups;
6205
+ }, [resolvedGenericGroups]);
6206
+ const makeServices = useCallback14(() => {
6207
+ return {
6208
+ host,
6209
+ activeSceneId,
6210
+ tracks: tracksRef.current,
6211
+ updateTrack,
6212
+ setTracks,
6213
+ reloadTracks: loadTracks,
6214
+ soundHistory,
6215
+ engineToDbId,
6216
+ trackDataKey,
6217
+ markEditLoaded,
6218
+ createFamilyTrack: (nameSuffix = "") => host.createTrack({
6219
+ name: `${identity.trackNamePrefix}-${Date.now()}${nameSuffix}`,
6220
+ ...adapter.createTrackOptions()
6221
+ }),
6222
+ resolvedGroups: (metaKey) => resolvedGenericGroupsRef.current[metaKey]?.resolved ?? []
6223
+ };
6224
+ }, [host, activeSceneId, updateTrack, loadTracks, soundHistory, engineToDbId, markEditLoaded, identity, adapter]);
6225
+ const handleGenerate = useCallback14(
6226
+ async (trackId) => {
6227
+ const track = tracks.find((t) => t.handle.id === trackId);
6228
+ if (!track || !track.prompt.trim()) return;
6229
+ if (!isAuthenticated) {
6230
+ host.showToast("warning", "Sign In Required", "Please sign in to generate MIDI");
6231
+ return;
6232
+ }
6233
+ setTracks(
6234
+ (prev) => prev.map(
6235
+ (t) => t.handle.id === trackId ? { ...t, isGenerating: true, error: null, generationProgress: 0 } : t
6236
+ )
6237
+ );
6238
+ try {
6239
+ await adapter.generation.generate(track, makeServices());
6240
+ } catch (error) {
6241
+ const msg = error instanceof Error ? error.message : "Generation failed";
6242
+ setTracks(
6243
+ (prev) => prev.map(
6244
+ (t) => t.handle.id === trackId ? { ...t, isGenerating: false, error: msg, generationProgress: 0 } : t
6245
+ )
6246
+ );
6247
+ host.showToast("error", "Generation failed", msg);
6248
+ }
6249
+ },
6250
+ [host, adapter, tracks, isAuthenticated, makeServices]
6251
+ );
6252
+ const handleMuteToggle = useCallback14(
6253
+ (trackId) => {
6254
+ const track = tracks.find((t) => t.handle.id === trackId);
6255
+ if (!track) return;
6256
+ const newMuted = !track.runtimeState.muted;
6257
+ setTracks(
6258
+ (prev) => prev.map(
6259
+ (t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, muted: newMuted } } : t
6260
+ )
6261
+ );
6262
+ host.setTrackMute(trackId, newMuted).catch(() => {
6263
+ setTracks(
6264
+ (prev) => prev.map(
6265
+ (t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, muted: !newMuted } } : t
6266
+ )
6267
+ );
6268
+ });
6269
+ },
6270
+ [host, tracks]
6271
+ );
6272
+ const handleSoloToggle = useCallback14(
6273
+ (trackId) => {
6274
+ const track = tracks.find((t) => t.handle.id === trackId);
6275
+ if (!track) return;
6276
+ const newSolo = !track.runtimeState.solo;
6277
+ setTracks(
6278
+ (prev) => prev.map(
6279
+ (t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, solo: newSolo } } : t
6280
+ )
6281
+ );
6282
+ host.setTrackSolo(trackId, newSolo).catch(() => {
6283
+ setTracks(
6284
+ (prev) => prev.map(
6285
+ (t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, solo: !newSolo } } : t
6286
+ )
6287
+ );
6288
+ });
6289
+ },
6290
+ [host, tracks]
6291
+ );
6292
+ const handleVolumeChange = useCallback14(
6293
+ (trackId, volume) => {
6294
+ setTracks(
6295
+ (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, volume } } : t)
6296
+ );
6297
+ host.setTrackVolume(trackId, volume).catch(() => {
6298
+ });
6299
+ },
6300
+ [host]
6301
+ );
6302
+ const handlePanChange = useCallback14(
6303
+ (trackId, pan) => {
6304
+ setTracks(
6305
+ (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, pan } } : t)
6306
+ );
6307
+ host.setTrackPan(trackId, pan).catch(() => {
6308
+ });
6309
+ },
6310
+ [host]
6311
+ );
6312
+ const handleShuffle = useCallback14(
6313
+ async (trackId) => {
6314
+ const track = tracks.find((t) => t.handle.id === trackId);
6315
+ if (!track) return;
6316
+ if (soundHistory.list(trackId).entries.length === 0) {
6317
+ try {
6318
+ const cap = await adapter.sound.captureSoundDescriptor(trackId);
6319
+ if (cap) soundHistory.record(trackId, cap.descriptor, adapter.sound.previousSoundLabel);
6320
+ } catch {
6321
+ }
6322
+ }
6323
+ try {
6324
+ let result;
6325
+ let nextHistory;
6326
+ try {
6327
+ result = await adapter.shuffle.shuffle(track, Array.from(track.shuffleHistory));
6328
+ nextHistory = new Set(track.shuffleHistory);
6329
+ } catch (firstErr) {
6330
+ if (adapter.shuffle.isExhaustedError(firstErr)) {
6331
+ nextHistory = /* @__PURE__ */ new Set();
6332
+ result = await adapter.shuffle.shuffle(track, []);
6333
+ } else {
6334
+ throw firstErr;
6335
+ }
6336
+ }
6337
+ nextHistory.add(result.appliedName);
6338
+ setTracks(
6339
+ (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, shuffleHistory: nextHistory } : t)
6340
+ );
6341
+ try {
6342
+ const cap = await adapter.sound.captureSoundDescriptor(trackId);
6343
+ if (cap) soundHistory.record(trackId, cap.descriptor, result.appliedName);
6344
+ } catch {
6345
+ }
6346
+ console.log(`[${logTag}] Sound shuffled: ${result.appliedName} (history ${nextHistory.size})`);
6347
+ } catch (error) {
6348
+ const msg = error instanceof Error ? error.message : "Shuffle failed";
6349
+ host.showToast("error", "Shuffle failed", msg);
6350
+ }
6351
+ },
6352
+ [host, adapter, tracks, soundHistory, logTag]
6353
+ );
6354
+ const handleCopy = useCallback14(
6355
+ async (trackId) => {
6356
+ try {
6357
+ const newHandle = await host.duplicateTrack(trackId);
6358
+ await loadTracks();
6359
+ host.showToast("success", "Track duplicated", newHandle.name);
6360
+ } catch (error) {
6361
+ const msg = error instanceof Error ? error.message : "Copy failed";
6362
+ host.showToast("error", "Copy failed", msg);
6363
+ }
6364
+ },
6365
+ [host, loadTracks]
6366
+ );
6367
+ const handleFxToggle = useCallback14(
6368
+ (trackId, category, enabled) => {
6369
+ setTracks(
6370
+ (prev) => prev.map(
6371
+ (t) => t.handle.id === trackId ? { ...t, fxDetailState: { ...t.fxDetailState, [category]: { ...t.fxDetailState[category], enabled } } } : t
6372
+ )
6373
+ );
6374
+ host.toggleTrackFx(trackId, category, enabled).catch(() => {
6375
+ setTracks(
6376
+ (prev) => prev.map(
6377
+ (t) => t.handle.id === trackId ? {
6378
+ ...t,
6379
+ fxDetailState: {
6380
+ ...t.fxDetailState,
6381
+ [category]: { ...t.fxDetailState[category], enabled: !enabled }
6382
+ }
6383
+ } : t
6384
+ )
6385
+ );
6386
+ });
6387
+ },
6388
+ [host]
6389
+ );
6390
+ const handleFxPresetChange = useCallback14(
6391
+ (trackId, category, presetIndex) => {
6392
+ setTracks(
6393
+ (prev) => prev.map(
6394
+ (t) => t.handle.id === trackId ? { ...t, fxDetailState: { ...t.fxDetailState, [category]: { ...t.fxDetailState[category], presetIndex } } } : t
6395
+ )
6396
+ );
6397
+ host.setTrackFxPreset(trackId, category, presetIndex).then((result) => {
6398
+ if (result.dryWet !== void 0) {
6399
+ setTracks(
6400
+ (prev) => prev.map(
6401
+ (t) => t.handle.id === trackId ? {
6402
+ ...t,
6403
+ fxDetailState: {
6404
+ ...t.fxDetailState,
6405
+ [category]: { ...t.fxDetailState[category], dryWet: result.dryWet }
6406
+ }
6407
+ } : t
6408
+ )
6409
+ );
6410
+ }
6411
+ }).catch(() => {
6412
+ });
6413
+ },
6414
+ [host]
6415
+ );
6416
+ const handleFxDryWetChange = useCallback14(
6417
+ (trackId, category, value) => {
6418
+ setTracks(
6419
+ (prev) => prev.map(
6420
+ (t) => t.handle.id === trackId ? { ...t, fxDetailState: { ...t.fxDetailState, [category]: { ...t.fxDetailState[category], dryWet: value } } } : t
6421
+ )
6422
+ );
6423
+ host.setTrackFxDryWet(trackId, category, value).catch(() => {
6424
+ });
6425
+ },
6426
+ [host]
6427
+ );
6428
+ const toggleFxDrawer = useCallback14(
6429
+ (trackId) => {
6430
+ setTracks(
6431
+ (prev) => prev.map((t) => {
6432
+ if (t.handle.id !== trackId) return t;
6433
+ const onFx = t.drawerOpen && t.drawerTab === "fx";
6434
+ return { ...t, drawerOpen: !onFx, drawerTab: "fx", editorStage: false };
6435
+ })
6436
+ );
6437
+ const track = tracks.find((t) => t.handle.id === trackId);
6438
+ const wasOnFx = !!track && track.drawerOpen && track.drawerTab === "fx";
6439
+ if (track && !wasOnFx) {
6440
+ host.getTrackFxState(trackId).then((fxState) => {
6441
+ setTracks(
6442
+ (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, fxDetailState: pluginFxToToggleFx(fxState) } : t)
6443
+ );
6444
+ }).catch(() => {
6445
+ });
6446
+ }
6447
+ },
6448
+ [host, tracks]
6449
+ );
6450
+ const loadEditNotes = useCallback14(
6451
+ async (trackId) => {
6452
+ try {
6453
+ const mc = await host.getMusicalContext();
6454
+ let notes = [];
6455
+ if (typeof host.readMidiNotes === "function") {
6456
+ const result = await host.readMidiNotes(trackId);
6457
+ notes = result.clips[0]?.notes ?? [];
6458
+ }
6459
+ setTracks(
6460
+ (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editNotes: notes, editBars: mc.bars, editBpm: mc.bpm } : t)
6461
+ );
6462
+ } catch (err) {
6463
+ console.warn(`[${logTag}] Failed to load MIDI for editing:`, err);
6464
+ }
6465
+ },
6466
+ [host, logTag]
6467
+ );
6468
+ const handleNotesChange = useCallback14(
6469
+ (trackId, notes) => {
6470
+ setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editNotes: notes } : t));
6471
+ const key = `edit:${trackId}`;
6472
+ if (saveTimeoutRefs.current[key]) {
6473
+ clearTimeout(saveTimeoutRefs.current[key]);
6474
+ }
6475
+ saveTimeoutRefs.current[key] = setTimeout(() => {
6476
+ void (async () => {
6477
+ try {
6478
+ if (notes.length === 0) {
6479
+ await host.clearMidi(trackId);
6480
+ } else {
6481
+ const mc = await host.getMusicalContext();
6482
+ await host.writeMidiClip(trackId, {
6483
+ startTime: 0,
6484
+ endTime: mc.bars * 4 * 60 / mc.bpm,
6485
+ tempo: mc.bpm,
6486
+ notes
6487
+ });
6488
+ }
6489
+ } catch (err) {
6490
+ const msg = err instanceof Error ? err.message : String(err);
6491
+ host.showToast("error", "Failed to save edit", msg);
6492
+ }
6493
+ })();
6494
+ }, 300);
6495
+ },
6496
+ [host]
6497
+ );
6498
+ const handleTabChange = useCallback14(
6499
+ (trackId, tab) => {
6500
+ setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, drawerOpen: true, drawerTab: tab } : t));
6501
+ if (tab === "fx") {
6502
+ host.getTrackFxState(trackId).then((fxState) => {
6503
+ setTracks(
6504
+ (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, fxDetailState: pluginFxToToggleFx(fxState) } : t)
6505
+ );
6506
+ }).catch(() => {
6507
+ });
6508
+ } else if (tab === "pick" && availableInstruments.length === 0 && !instrumentsLoading) {
6509
+ setInstrumentsLoading(true);
6510
+ host.getAvailableInstruments().then((instruments) => {
6511
+ setAvailableInstruments(instruments);
6512
+ }).catch(() => {
6513
+ }).finally(() => {
6514
+ setInstrumentsLoading(false);
6515
+ });
6516
+ } else if (tab === "edit" && !editLoadStartedRef.current.has(trackId)) {
6517
+ editLoadStartedRef.current.add(trackId);
6518
+ void loadEditNotes(trackId);
6519
+ }
6520
+ },
6521
+ [host, availableInstruments.length, instrumentsLoading, loadEditNotes]
6522
+ );
6523
+ const handleProgressChange = useCallback14((trackId, pct) => {
6524
+ setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, generationProgress: pct } : t));
6525
+ }, []);
6526
+ const handleToggleDrawer = useCallback14((trackId) => {
6527
+ setTracks(
6528
+ (prev) => prev.map((t) => {
6529
+ if (t.handle.id !== trackId) return t;
6530
+ const onSound = t.drawerOpen && t.drawerTab !== "fx";
6531
+ return { ...t, drawerOpen: !onSound, drawerTab: "history", editorStage: false };
6532
+ })
6533
+ );
6534
+ }, []);
6535
+ const handleInstrumentSelect = useCallback14(
6536
+ async (trackId, pluginId) => {
6537
+ const isDefaultInstrument = pluginId === (identity.defaultInstrumentPluginId ?? "Surge XT");
6538
+ if (isDefaultInstrument) {
6539
+ setTracks(
6540
+ (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, drawerOpen: false, editorStage: false } : t)
6541
+ );
6542
+ try {
6543
+ await host.setTrackInstrument(trackId, pluginId);
6544
+ const descriptor = await host.getTrackInstrument(trackId);
6545
+ setTracks(
6546
+ (prev) => prev.map(
6547
+ (t) => t.handle.id === trackId ? {
6548
+ ...t,
6549
+ instrumentPluginId: descriptor?.pluginId ?? null,
6550
+ instrumentName: descriptor?.name ?? null,
6551
+ instrumentMissing: descriptor?.missing ?? false
6552
+ } : t
6553
+ )
6554
+ );
6555
+ } catch (err) {
6556
+ const msg = err instanceof Error ? err.message : "Failed to load instrument";
6557
+ host.showToast("error", "Instrument load failed", msg);
6558
+ }
6559
+ return;
6560
+ }
6561
+ setTracks(
6562
+ (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, drawerTab: "pick", editorStage: true } : t)
6563
+ );
6564
+ try {
6565
+ await host.setTrackInstrument(trackId, pluginId);
6566
+ const descriptor = await host.getTrackInstrument(trackId);
6567
+ setTracks(
6568
+ (prev) => prev.map(
6569
+ (t) => t.handle.id === trackId ? {
6570
+ ...t,
6571
+ instrumentPluginId: descriptor?.pluginId ?? null,
6572
+ instrumentName: descriptor?.name ?? null,
6573
+ instrumentMissing: descriptor?.missing ?? false
6574
+ } : t
6575
+ )
6576
+ );
6577
+ } catch (err) {
6578
+ const msg = err instanceof Error ? err.message : "Failed to load instrument";
6579
+ console.error(`[${logTag}] Failed to set instrument:`, err);
6580
+ host.showToast("error", "Instrument load failed", msg);
6581
+ setTracks(
6582
+ (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editorStage: false } : t)
6583
+ );
6584
+ }
6585
+ },
6586
+ [host, identity.defaultInstrumentPluginId, logTag]
6587
+ );
6588
+ const handleShowEditor = useCallback14(
6589
+ async (trackId) => {
6590
+ try {
6591
+ await host.showInstrumentEditor(trackId);
6592
+ } catch (err) {
6593
+ const msg = err instanceof Error ? err.message : "Failed to open editor";
6594
+ host.showToast("error", "Editor failed", msg);
6595
+ }
6596
+ },
6597
+ [host]
6598
+ );
6599
+ const handleBackToInstruments = useCallback14((trackId) => {
6600
+ setTracks(
6601
+ (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editorStage: false } : t)
6602
+ );
6603
+ }, []);
6604
+ const handleRefreshInstruments = useCallback14(() => {
6605
+ setInstrumentsLoading(true);
6606
+ host.getAvailableInstruments().then((instruments) => {
6607
+ setAvailableInstruments(instruments);
6608
+ }).catch(() => {
6609
+ }).finally(() => {
6610
+ setInstrumentsLoading(false);
6611
+ });
6612
+ }, [host]);
6613
+ const onAuditionNote = useCallback14(
6614
+ (trackId, pitch, velocity, ms) => {
6615
+ void host.auditionNote(trackId, pitch, velocity, ms);
6616
+ },
6617
+ [host]
6618
+ );
6619
+ const { resolvedCrossfadePairs, crossfadeMemberDbIds } = useMemo8(() => {
6620
+ const byDbId = new Map(tracks.map((t) => [t.handle.dbId, t]));
6621
+ const pairs = [];
6622
+ const members = /* @__PURE__ */ new Set();
6623
+ for (const p of crossfadePairsMeta) {
6624
+ const origin = byDbId.get(p.originDbId);
6625
+ const target = byDbId.get(p.targetDbId);
6626
+ if (origin && target) {
6627
+ pairs.push({ ...p, origin, target });
6628
+ members.add(p.originDbId);
6629
+ members.add(p.targetDbId);
6630
+ }
6631
+ }
6632
+ return { resolvedCrossfadePairs: pairs, crossfadeMemberDbIds: members };
6633
+ }, [tracks, crossfadePairsMeta]);
6634
+ const { resolvedFades, fadeMemberDbIds } = useMemo8(() => {
6635
+ const byDbId = new Map(tracks.map((t) => [t.handle.dbId, t]));
6636
+ const list = [];
6637
+ const members = /* @__PURE__ */ new Set();
6638
+ for (const f of fadesMeta) {
6639
+ const track = byDbId.get(f.dbId);
6640
+ if (track) {
6641
+ list.push({ ...f, track });
6642
+ members.add(f.dbId);
6643
+ }
6644
+ }
6645
+ return { resolvedFades: list, fadeMemberDbIds: members };
6646
+ }, [tracks, fadesMeta]);
6647
+ const transition = useTransitionOps({
6648
+ host,
6649
+ adapter,
6650
+ activeSceneId,
6651
+ isConnected,
6652
+ isAuthenticated,
6653
+ sceneContext,
6654
+ tracks,
6655
+ setTracks,
6656
+ loadTracks,
6657
+ setCrossfadePairsMeta,
6658
+ setFadesMeta,
6659
+ resolvedCrossfadePairs,
6660
+ resolvedFades
6661
+ });
6662
+ const setGroupMute = useCallback14(
6663
+ (trackIds, muted) => {
6664
+ for (const id of trackIds) {
6665
+ setTracks(
6666
+ (prev) => prev.map((t) => t.handle.id === id ? { ...t, runtimeState: { ...t.runtimeState, muted } } : t)
6667
+ );
6668
+ host.setTrackMute(id, muted).catch(() => {
6669
+ });
6670
+ }
6671
+ },
6672
+ [host]
6673
+ );
6674
+ const setGroupSolo = useCallback14(
6675
+ (trackIds, solo) => {
6676
+ for (const id of trackIds) {
6677
+ setTracks(
6678
+ (prev) => prev.map((t) => t.handle.id === id ? { ...t, runtimeState: { ...t.runtimeState, solo } } : t)
6679
+ );
6680
+ host.setTrackSolo(id, solo).catch(() => {
6681
+ });
6682
+ }
6683
+ },
6684
+ [host]
6685
+ );
6686
+ const deleteGroup = useCallback14(
6687
+ async (members, cleanupKeySuffixes) => {
6688
+ for (const member of members) {
6689
+ try {
6690
+ await host.deleteTrack(member.engineId);
6691
+ } catch {
6692
+ }
6693
+ if (activeSceneId) {
6694
+ for (const suffix of cleanupKeySuffixes) {
6695
+ await host.deleteSceneData(activeSceneId, trackDataKey(member.dbId, suffix)).catch(() => {
6696
+ });
6697
+ }
6698
+ }
6699
+ }
6700
+ const gone = new Set(members.map((m) => m.engineId));
6701
+ setTracks((prev) => prev.filter((t) => !gone.has(t.handle.id)));
6702
+ await loadTracks(true);
6703
+ },
6704
+ [host, activeSceneId, loadTracks]
6705
+ );
6706
+ const handlers = useMemo8(
6707
+ () => ({
6708
+ promptChange: handlePromptChange,
6709
+ generate: (trackId) => {
6710
+ void handleGenerate(trackId);
6711
+ },
6712
+ shuffle: (trackId) => {
6713
+ void handleShuffle(trackId);
6714
+ },
6715
+ copy: (trackId) => {
6716
+ void handleCopy(trackId);
6717
+ },
6718
+ delete: (trackId) => {
6719
+ void handleDeleteTrack(trackId);
6720
+ },
6721
+ muteToggle: handleMuteToggle,
6722
+ soloToggle: handleSoloToggle,
6723
+ volumeChange: handleVolumeChange,
6724
+ panChange: handlePanChange,
6725
+ tabChange: handleTabChange,
6726
+ toggleDrawer: handleToggleDrawer,
6727
+ toggleFxDrawer,
6728
+ notesChange: handleNotesChange,
6729
+ progressChange: handleProgressChange
6730
+ }),
6731
+ [
6732
+ handlePromptChange,
6733
+ handleGenerate,
6734
+ handleShuffle,
6735
+ handleCopy,
6736
+ handleDeleteTrack,
6737
+ handleMuteToggle,
6738
+ handleSoloToggle,
6739
+ handleVolumeChange,
6740
+ handlePanChange,
6741
+ handleTabChange,
6742
+ handleToggleDrawer,
6743
+ toggleFxDrawer,
6744
+ handleNotesChange,
6745
+ handleProgressChange
6746
+ ]
6747
+ );
6748
+ return {
6749
+ ui,
6750
+ adapter,
6751
+ tracks,
6752
+ setTracks,
6753
+ isLoadingTracks,
6754
+ loadTracks,
6755
+ engineToDbId,
6756
+ supportsMeters,
6757
+ trackLevels,
6758
+ anySolo,
6759
+ reorder,
6760
+ soundHistory,
6761
+ isComposing,
6762
+ placeholders,
6763
+ isAddingTrack,
6764
+ isExportingMidi,
6765
+ designerView,
6766
+ canCrossfade,
6767
+ needsContract,
6768
+ xfFromId,
6769
+ xfToId,
6770
+ importOpen,
6771
+ setImportOpen,
6772
+ soundImportTarget,
6773
+ setSoundImportTarget,
6774
+ handleSoundImportPick,
6775
+ handlePortTrack,
6776
+ transition,
6777
+ crossfadePairsMeta,
6778
+ fadesMeta,
6779
+ resolvedCrossfadePairs,
6780
+ crossfadeMemberDbIds,
6781
+ resolvedFades,
6782
+ fadeMemberDbIds,
6783
+ resolvedGenericGroups,
6784
+ genericGroupMemberDbIds,
6785
+ availableInstruments,
6786
+ instrumentsLoading,
6787
+ handlers,
6788
+ handleGenerate,
6789
+ handleShuffle,
6790
+ handleAddTrack,
6791
+ handleDeleteTrack,
6792
+ handleExportMidi,
6793
+ handlePromptChange,
6794
+ handleMuteToggle,
6795
+ handleSoloToggle,
6796
+ handleVolumeChange,
6797
+ handlePanChange,
6798
+ handleTabChange,
6799
+ handleToggleDrawer,
6800
+ toggleFxDrawer,
6801
+ handleNotesChange,
6802
+ handleProgressChange,
6803
+ handleCopy,
6804
+ handleFxToggle,
6805
+ handleFxPresetChange,
6806
+ handleFxDryWetChange,
6807
+ handleInstrumentSelect,
6808
+ handleShowEditor,
6809
+ handleBackToInstruments,
6810
+ handleRefreshInstruments,
6811
+ onAuditionNote,
6812
+ makeServices,
6813
+ setGroupMute,
6814
+ setGroupSolo,
6815
+ deleteGroup
6816
+ };
6817
+ }
6818
+
6819
+ // src/panel-core/GeneratorPanelShell.tsx
6820
+ import React20, { useCallback as useCallback15 } from "react";
6821
+ import { Fragment as Fragment6, jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
6822
+ function GeneratorPanelShell({ core, slots }) {
6823
+ const {
6824
+ ui,
6825
+ adapter,
6826
+ tracks,
6827
+ isLoadingTracks,
6828
+ supportsMeters,
6829
+ trackLevels,
6830
+ anySolo,
6831
+ reorder,
6832
+ soundHistory,
6833
+ isComposing,
6834
+ placeholders,
6835
+ designerView,
6836
+ canCrossfade,
6837
+ xfFromId,
6838
+ xfToId,
6839
+ importOpen,
6840
+ setImportOpen,
6841
+ soundImportTarget,
6842
+ setSoundImportTarget,
6843
+ handleSoundImportPick,
6844
+ handlePortTrack,
6845
+ transition,
6846
+ crossfadePairsMeta,
6847
+ fadesMeta,
6848
+ resolvedCrossfadePairs,
6849
+ crossfadeMemberDbIds,
6850
+ resolvedFades,
6851
+ fadeMemberDbIds,
6852
+ resolvedGenericGroups,
6853
+ genericGroupMemberDbIds,
6854
+ availableInstruments,
6855
+ instrumentsLoading,
6856
+ handlers,
6857
+ isExportingMidi,
6858
+ handleExportMidi,
6859
+ handleFxToggle,
6860
+ handleFxPresetChange,
6861
+ handleFxDryWetChange,
6862
+ handleInstrumentSelect,
6863
+ handleShowEditor,
6864
+ handleBackToInstruments,
6865
+ handleRefreshInstruments,
6866
+ onAuditionNote,
6867
+ loadTracks,
6868
+ makeServices,
6869
+ setGroupMute,
6870
+ setGroupSolo,
6871
+ deleteGroup
6872
+ } = core;
6873
+ const { host, activeSceneId, isAuthenticated, sceneContext, onSelectScene, onOpenContract } = ui;
6874
+ const { identity, features } = adapter;
6875
+ const buildRowProps = useCallback15(
6876
+ (track, drag) => {
6877
+ const id = track.handle.id;
6878
+ const pickerProps = features.instrumentPicker ? {
6879
+ instrumentName: track.instrumentName,
6880
+ instrumentMissing: track.instrumentMissing,
6881
+ onToggleDrawer: () => handlers.toggleDrawer(id),
6882
+ availableInstruments,
6883
+ currentInstrumentPluginId: track.instrumentPluginId,
6884
+ onInstrumentSelect: (pluginId) => handleInstrumentSelect(id, pluginId),
6885
+ instrumentsLoading,
6886
+ onRefreshInstruments: handleRefreshInstruments,
6887
+ editorStage: track.editorStage,
6888
+ onShowEditor: () => handleShowEditor(id),
6889
+ onBackToInstruments: () => handleBackToInstruments(id)
6890
+ } : {};
6891
+ const importSoundProps = features.importTracks ? {
6892
+ onImportSound: () => setSoundImportTarget(track),
6893
+ importSoundLabel: adapter.sound.importSoundLabel
6894
+ } : {};
6895
+ const props = {
6896
+ ...drag ? { drag } : {},
6897
+ track: { id, name: track.handle.name, role: track.role },
6898
+ levels: supportsMeters ? trackLevels : void 0,
6899
+ prompt: track.prompt,
6900
+ runtimeState: {
6901
+ muted: track.runtimeState.muted,
6902
+ solo: track.runtimeState.solo,
6903
+ volume: track.runtimeState.volume,
6904
+ pan: track.runtimeState.pan
6905
+ },
6906
+ soloedOut: anySolo && !track.runtimeState.solo,
6907
+ fxDetailState: track.fxDetailState,
6908
+ drawerOpen: track.drawerOpen,
6909
+ drawerTab: track.drawerTab,
6910
+ onTabChange: (tab) => handlers.tabChange(id, tab),
6911
+ isGenerating: track.isGenerating,
6912
+ isAuthenticated,
6913
+ error: track.error,
6914
+ hasMidi: track.hasMidi,
6915
+ generationProgress: track.generationProgress,
6916
+ estimatedGenerationMs: identity.estimatedGenerationMs,
6917
+ onPromptChange: (prompt) => handlers.promptChange(id, prompt),
6918
+ onGenerate: () => handlers.generate(id),
6919
+ onShuffle: () => handlers.shuffle(id),
6920
+ onCopy: () => handlers.copy(id),
6921
+ onDelete: () => handlers.delete(id),
6922
+ onMuteToggle: () => handlers.muteToggle(id),
6923
+ onSoloToggle: () => handlers.soloToggle(id),
6924
+ onVolumeChange: (vol) => handlers.volumeChange(id, vol),
6925
+ onPanChange: (pan) => handlers.panChange(id, pan),
6926
+ onFxToggle: (cat, enabled) => handleFxToggle(id, cat, enabled),
6927
+ onFxPresetChange: (cat, idx) => handleFxPresetChange(id, cat, idx),
6928
+ onFxDryWetChange: (cat, val) => handleFxDryWetChange(id, cat, val),
6929
+ onToggleFxDrawer: () => handlers.toggleFxDrawer(id),
6930
+ onProgressChange: (pct) => handlers.progressChange(id, pct),
6931
+ accentColor: identity.accentColor,
6932
+ ...pickerProps,
6933
+ soundHistory: soundHistory.list(id).entries,
6934
+ soundHistoryCursor: soundHistory.list(id).cursor,
6935
+ onRestoreSound: (i) => {
6936
+ void soundHistory.restoreTo(id, i);
6937
+ },
6938
+ onToggleFavorite: (i) => soundHistory.toggleFavorite(id, i),
6939
+ ...importSoundProps,
6940
+ editNotes: track.editNotes,
6941
+ onNotesChange: (notes) => handlers.notesChange(id, notes),
6942
+ editBars: track.editBars,
6943
+ editBpm: track.editBpm,
6944
+ editSnap: 0.25,
6945
+ onAuditionNote: (pitch, vel, ms) => onAuditionNote(id, pitch, vel, ms)
6946
+ };
6947
+ return adapter.mapTrackRowProps ? adapter.mapTrackRowProps(track, props) : props;
6948
+ },
6949
+ [
6950
+ features.instrumentPicker,
6951
+ features.importTracks,
6952
+ adapter,
6953
+ supportsMeters,
6954
+ trackLevels,
6955
+ anySolo,
6956
+ isAuthenticated,
6957
+ identity,
6958
+ handlers,
6959
+ availableInstruments,
6960
+ instrumentsLoading,
6961
+ handleInstrumentSelect,
6962
+ handleRefreshInstruments,
6963
+ handleShowEditor,
6964
+ handleBackToInstruments,
6965
+ setSoundImportTarget,
6966
+ soundHistory,
6967
+ handleFxToggle,
6968
+ handleFxPresetChange,
6969
+ handleFxDryWetChange,
6970
+ onAuditionNote
6971
+ ]
6972
+ );
6973
+ if (!activeSceneId) {
6974
+ return /* @__PURE__ */ jsx24(
6975
+ "div",
6976
+ {
6977
+ "data-testid": `no-scene-placeholder-${identity.familyKey}`,
6978
+ className: "flex items-center justify-center py-8",
6979
+ children: /* @__PURE__ */ jsx24(
6980
+ "button",
6981
+ {
6982
+ onClick: () => onSelectScene?.(),
6983
+ className: "text-sas-muted text-xs hover:text-sas-accent transition-colors underline underline-offset-2",
6984
+ children: "Select a Scene"
6985
+ }
6986
+ )
6987
+ }
6988
+ );
6989
+ }
6990
+ if (!sceneContext?.hasContract) {
6991
+ return /* @__PURE__ */ jsx24(
6992
+ "div",
6993
+ {
6994
+ "data-testid": `no-contract-placeholder-${identity.familyKey}`,
6995
+ className: "flex items-center justify-center py-8",
6996
+ children: /* @__PURE__ */ jsx24(
6997
+ "button",
6998
+ {
6999
+ onClick: () => onOpenContract?.(),
7000
+ className: "text-sas-muted text-xs hover:text-sas-accent transition-colors underline underline-offset-2",
7001
+ children: "Generate a Contract"
7002
+ }
7003
+ )
7004
+ }
7005
+ );
7006
+ }
7007
+ if (features.bulkComposePlaceholders && isComposing) {
7008
+ return /* @__PURE__ */ jsx24("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2", children: /* @__PURE__ */ jsx24(SorceryProgressBar, { isLoading: true, statusText: "COMPOSING...", heightClass: "h-10" }) });
7009
+ }
7010
+ const activePlaceholders = features.bulkComposePlaceholders ? placeholders : [];
7011
+ if (activePlaceholders.length > 0) {
7012
+ const tracksByDbId = /* @__PURE__ */ new Map();
7013
+ for (const t of tracks) {
7014
+ tracksByDbId.set(t.handle.dbId, t);
7015
+ if (t.handle.id !== t.handle.dbId) {
7016
+ tracksByDbId.set(t.handle.id, t);
7017
+ }
7018
+ }
7019
+ return /* @__PURE__ */ jsx24("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: activePlaceholders.map((ph) => {
7020
+ const loadedTrack = ph.status === "completed" ? tracksByDbId.get(ph.id) : void 0;
7021
+ if (loadedTrack) {
7022
+ return /* @__PURE__ */ jsx24(TrackRow, { ...buildRowProps(loadedTrack) }, ph.id);
7023
+ }
7024
+ return /* @__PURE__ */ jsx24(
7025
+ "div",
7026
+ {
7027
+ "data-testid": "bulk-placeholder-track",
7028
+ className: "relative rounded-sm border w-full overflow-hidden border-sas-border bg-sas-panel-alt",
7029
+ style: { borderLeftColor: identity.placeholderAccentColor, borderLeftWidth: "3px" },
7030
+ children: /* @__PURE__ */ jsx24(SorceryProgressBar, { isLoading: true, statusText: "CONJURING MIDI...", heightClass: "h-10" })
7031
+ },
7032
+ ph.id
7033
+ );
7034
+ }) });
7035
+ }
7036
+ const groupCtx = {
7037
+ services: makeServices(),
7038
+ anySolo,
7039
+ supportsMeters,
7040
+ levels: supportsMeters ? trackLevels : void 0,
7041
+ handlers,
7042
+ renderDefaultTrackRow: (track, overrides, drag) => /* @__PURE__ */ jsx24(TrackRow, { ...{ ...buildRowProps(track, drag), ...overrides ?? {} } }, track.handle.id),
7043
+ setGroupMute,
7044
+ setGroupSolo,
7045
+ deleteGroup
7046
+ };
7047
+ return /* @__PURE__ */ jsxs18("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: [
7048
+ features.importTracks && host.listImportableTracks && /* @__PURE__ */ jsx24(
7049
+ ImportTrackModal,
7050
+ {
7051
+ host,
7052
+ open: importOpen,
7053
+ onClose: () => setImportOpen(false),
7054
+ onImported: () => {
7055
+ void loadTracks(true);
7056
+ },
7057
+ onPortTrack: host.readImportableTrackMidi ? handlePortTrack : void 0,
7058
+ testIdPrefix: `${identity.familyKey}-import`
7059
+ }
7060
+ ),
7061
+ features.importTracks && host.listImportableTracks && host.getTrackSound && /* @__PURE__ */ jsx24(
7062
+ ImportTrackModal,
7063
+ {
7064
+ host,
7065
+ mode: "sound",
7066
+ open: !!soundImportTarget,
7067
+ title: adapter.sound.importSoundLabel,
7068
+ onClose: () => setSoundImportTarget(null),
7069
+ onImported: () => {
7070
+ },
7071
+ onPick: handleSoundImportPick,
7072
+ testIdPrefix: `${identity.familyKey}-sound-import`
7073
+ }
7074
+ ),
7075
+ slots?.modals,
7076
+ canCrossfade && xfFromId && xfToId && /* @__PURE__ */ jsx24("div", { className: designerView ? "contents" : "hidden", children: /* @__PURE__ */ jsx24(
7077
+ TransitionDesigner,
7078
+ {
7079
+ host,
7080
+ fromSceneId: xfFromId,
7081
+ toSceneId: xfToId,
7082
+ transitionSceneId: activeSceneId ?? "",
7083
+ excludeSourceDbIds: [
7084
+ ...crossfadePairsMeta.flatMap((p) => [p.originSourceDbId, p.targetSourceDbId]),
7085
+ ...fadesMeta.map((f) => f.meta.sourceTrackDbId)
7086
+ ],
7087
+ onCreateCrossfade: transition.handleCreateCrossfade,
7088
+ onCreateFade: transition.handleCreateFade,
7089
+ familyLabel: identity.familyLabel,
7090
+ testIdPrefix: `${identity.familyKey}-transition-designer`
7091
+ }
7092
+ ) }),
7093
+ !(designerView && canCrossfade) && (isLoadingTracks ? /* @__PURE__ */ jsx24("div", { className: "text-sas-muted text-xs text-center py-4", children: "Loading tracks..." }) : /* @__PURE__ */ jsxs18(Fragment6, { children: [
7094
+ slots?.beforeRows,
7095
+ resolvedCrossfadePairs.map((pair) => /* @__PURE__ */ jsx24(
7096
+ CrossfadeTrackRow,
7097
+ {
7098
+ accentColor: identity.transitionAccentColor,
7099
+ levels: supportsMeters ? trackLevels : void 0,
7100
+ sliderPos: pair.sliderPos,
7101
+ origin: {
7102
+ trackId: pair.origin.handle.id,
7103
+ name: pair.origin.handle.name,
7104
+ role: pair.origin.role,
7105
+ sourceName: pair.originSourceName,
7106
+ soundLabel: pair.originSoundLabel,
7107
+ runtimeState: pair.origin.runtimeState
7108
+ },
7109
+ target: {
7110
+ trackId: pair.target.handle.id,
7111
+ name: pair.target.handle.name,
7112
+ role: pair.target.role,
7113
+ sourceName: pair.targetSourceName,
7114
+ soundLabel: pair.targetSoundLabel,
7115
+ runtimeState: pair.target.runtimeState
7116
+ },
7117
+ onMuteToggle: () => transition.handleCrossfadeMute(pair),
7118
+ onSoloToggle: () => transition.handleCrossfadeSolo(pair),
7119
+ onVolumeChange: (slot, vol) => handlers.volumeChange(
7120
+ slot === "origin" ? pair.origin.handle.id : pair.target.handle.id,
7121
+ vol
7122
+ ),
7123
+ onPanChange: (slot, pan) => handlers.panChange(
7124
+ slot === "origin" ? pair.origin.handle.id : pair.target.handle.id,
7125
+ pan
7126
+ ),
7127
+ onSliderChange: (pos) => transition.handleCrossfadeSlider(pair, pos),
7128
+ onDelete: () => transition.handleCrossfadeDelete(pair)
7129
+ },
7130
+ pair.groupId
7131
+ )),
7132
+ resolvedFades.map((fade) => /* @__PURE__ */ jsx24(
7133
+ FadeTrackRow,
7134
+ {
7135
+ accentColor: identity.transitionAccentColor,
7136
+ levels: supportsMeters ? trackLevels : void 0,
7137
+ direction: fade.meta.direction,
7138
+ gesture: fade.meta.gesture,
7139
+ sliderPos: fade.meta.sliderPos,
7140
+ layer: {
7141
+ trackId: fade.track.handle.id,
7142
+ name: fade.track.handle.name,
7143
+ role: fade.track.role,
7144
+ sourceName: fade.meta.sourceName,
7145
+ soundLabel: fade.meta.soundLabel,
7146
+ runtimeState: fade.track.runtimeState
7147
+ },
7148
+ onMuteToggle: () => handlers.muteToggle(fade.track.handle.id),
7149
+ onSoloToggle: () => handlers.soloToggle(fade.track.handle.id),
7150
+ onVolumeChange: (vol) => handlers.volumeChange(fade.track.handle.id, vol),
7151
+ onPanChange: (pan) => handlers.panChange(fade.track.handle.id, pan),
7152
+ onSliderChange: (pos) => transition.handleFadeSlider(fade, pos),
7153
+ onDelete: () => transition.handleFadeDelete(fade)
7154
+ },
7155
+ fade.dbId
7156
+ )),
7157
+ (adapter.groupExtensions ?? []).flatMap(
7158
+ (ext) => (resolvedGenericGroups[ext.metaKey]?.resolved ?? []).map((group) => /* @__PURE__ */ jsx24(React20.Fragment, { children: ext.renderGroup(group, groupCtx) }, `${ext.metaKey}:${group.groupId}`))
7159
+ ),
7160
+ tracks.map((track, index) => {
7161
+ if (crossfadeMemberDbIds.has(track.handle.dbId) || fadeMemberDbIds.has(track.handle.dbId) || genericGroupMemberDbIds.has(track.handle.dbId)) {
7162
+ return null;
7163
+ }
7164
+ return /* @__PURE__ */ jsx24(TrackRow, { ...buildRowProps(track, reorder.dragPropsFor(index)) }, track.handle.id);
7165
+ }),
7166
+ slots?.afterRows
7167
+ ] })),
7168
+ features.exportMidi && !designerView && !isLoadingTracks && tracks.length > 0 && (() => {
7169
+ const hasAnyMidi = tracks.some((t) => t.hasMidi);
7170
+ const exportDisabled = isExportingMidi || !hasAnyMidi;
7171
+ return /* @__PURE__ */ jsx24("div", { className: "pt-2", children: /* @__PURE__ */ jsx24(
7172
+ "button",
7173
+ {
7174
+ "data-testid": "export-midi-tracks-button",
7175
+ onClick: handleExportMidi,
7176
+ disabled: exportDisabled,
7177
+ title: isExportingMidi ? "Exporting..." : !hasAnyMidi ? "Generate MIDI on at least one track first" : "Export all tracks as a ZIP of .mid files",
7178
+ className: `w-full px-2 py-1.5 text-[10px] uppercase tracking-wide rounded-sm border transition-colors ${exportDisabled ? "text-sas-muted/40 border-transparent hover:border-sas-accent cursor-not-allowed" : "text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent"}`,
7179
+ children: isExportingMidi ? "Exporting..." : "Export Tracks"
7180
+ }
7181
+ ) });
7182
+ })()
7183
+ ] });
7184
+ }
7185
+
7186
+ // src/panel-core/surge-sound-adapter.ts
7187
+ async function getInstrument(host, trackId) {
7188
+ try {
7189
+ const plugins = await host.getTrackPlugins(trackId);
7190
+ const instrument = plugins.find(
7191
+ (p) => !p.name.includes("Volume") && !p.name.includes("Pan") && !p.name.includes("Level")
7192
+ );
7193
+ if (!instrument) return null;
7194
+ return { index: instrument.index, isRaw: !instrument.name.includes("Surge") };
7195
+ } catch {
7196
+ return null;
7197
+ }
7198
+ }
7199
+ function createSurgeSoundAdapter(host, overrides = {}) {
7200
+ const applySound = async (trackId, descriptor) => {
7201
+ const { state, stateType } = descriptor;
7202
+ const inst = await getInstrument(host, trackId);
7203
+ if (!inst) return;
7204
+ if (stateType === "raw") await host.setRawPluginState(trackId, inst.index, state);
7205
+ else await host.setPluginState(trackId, inst.index, state);
7206
+ };
7207
+ return {
7208
+ applySound,
7209
+ captureSoundDescriptor: async (trackId) => {
7210
+ const inst = await getInstrument(host, trackId);
7211
+ if (!inst) return null;
7212
+ const state = inst.isRaw ? await host.getRawPluginState(trackId, inst.index) : await host.getPluginState(trackId, inst.index);
7213
+ return { descriptor: { state, stateType: inst.isRaw ? "raw" : "valuetree" } };
7214
+ },
7215
+ copySnapshot: async (trackId, snap) => {
7216
+ if (snap.kind !== "preset") return "default";
7217
+ await applySound(trackId, { state: snap.state, stateType: snap.stateType });
7218
+ await host.persistTrackPresetState?.(trackId, {
7219
+ state: snap.state,
7220
+ stateType: snap.stateType ?? "valuetree",
7221
+ name: snap.label
7222
+ }).catch(() => {
7223
+ });
7224
+ return snap.label;
7225
+ },
7226
+ descriptorFromSnapshot: (snap) => {
7227
+ const preset = snap;
7228
+ return { state: preset.state, stateType: preset.stateType };
7229
+ },
7230
+ acceptedSnapshotKind: "preset",
7231
+ historyMax: overrides.historyMax ?? 12,
7232
+ importSoundLabel: overrides.importSoundLabel ?? "Import Preset",
7233
+ importNoun: "preset",
7234
+ previousSoundLabel: "Previous preset"
7235
+ };
7236
+ }
7237
+
4917
7238
  // src/constants/sdk-version.ts
4918
- var PLUGIN_SDK_VERSION = "2.34.0";
7239
+ var PLUGIN_SDK_VERSION = "2.35.0";
4919
7240
 
4920
7241
  // src/utils/format-concurrent-tracks.ts
4921
7242
  function formatConcurrentTracks(ctx) {
@@ -5081,6 +7402,7 @@ export {
5081
7402
  FadeTrackRow,
5082
7403
  FxToggleBar,
5083
7404
  GUTTER_W,
7405
+ GeneratorPanelShell,
5084
7406
  ImportTrackModal,
5085
7407
  TrackDrawer as InstrumentDrawer,
5086
7408
  LevelMeter,
@@ -5118,6 +7440,7 @@ export {
5118
7440
  cellToPx,
5119
7441
  centerScrollTop,
5120
7442
  computePeaks,
7443
+ createSurgeSoundAdapter,
5121
7444
  dbIdsFromKeys,
5122
7445
  dbToSlider,
5123
7446
  defaultFadeGesture,
@@ -5125,16 +7448,21 @@ export {
5125
7448
  formatConcurrentTracks,
5126
7449
  hashString,
5127
7450
  moveItem,
7451
+ newTrackState,
5128
7452
  normalizeSlots,
5129
7453
  padPair,
5130
7454
  padSlots,
5131
7455
  parseCrossfadePairs,
5132
7456
  parseFades,
7457
+ parseLLMNoteResponse,
7458
+ parseTrackGroups,
5133
7459
  pickTopKWeighted,
5134
7460
  pitchToName,
7461
+ pluginFxToToggleFx,
5135
7462
  pxToCell,
5136
7463
  reconcileSlots,
5137
7464
  resizeNoteDuration,
7465
+ resolveTrackGroups,
5138
7466
  rowKey,
5139
7467
  rowType,
5140
7468
  scorePromptMatch,
@@ -5143,14 +7471,17 @@ export {
5143
7471
  soundIdentity,
5144
7472
  synthesizeCuePoints,
5145
7473
  tokenizePrompt,
7474
+ trackDataKey,
5146
7475
  transposeNotes,
5147
7476
  useAnySolo,
7477
+ useGeneratorPanelCore,
5148
7478
  useSceneState,
5149
7479
  useSoundHistory,
5150
7480
  useTrackLevel,
5151
7481
  useTrackLevels,
5152
7482
  useTrackMeter,
5153
7483
  useTrackReorder,
7484
+ useTransitionOps,
5154
7485
  useTransportPlaying
5155
7486
  };
5156
7487
  //# sourceMappingURL=index.mjs.map