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