Each item computes its own target track as srcIdx + crossOffset.
Items from different source tracks land on different target tracks,
preserving the group's vertical layout shape.
Remove all cross-track logic in multiIds path. Each item only
updates its time position (delta) on its current track. No
merging or track reassignment.
Restore multiIds cross-track move for all selected items.
Clamp targetTrackId to first track. Auto-add new track via
addNewTrack() when dragging below the last track.
Cache PromptTemplateManager in ref to prevent new instance per
render. Filter: show only Rearrange/favorites when MIDI item
selected, all presets otherwise. Root cause of 'suggestion added
at top' was stale array reference from new instance every render.
Always show full preset list instead of filtering to rearrange/
favorites when item selected. Prevents suggestion list from
visually changing during selection/deselection.
Remove !st.isArmed guard in note-off handler. Stop note on ALL
tracks regardless of ARM state — FluidSynth stopNote on inactive
channel/pitch is a no-op, so this is safe.
- Master strip: add fader placeholder + VU meter bar
- MixerStrip: items-end -> items-stretch so fader + VU fill height
- VU meter: bar height maps volumeDb, color green/amber/red
- Track name uses track.color instead of fixed text-zinc-400
Add MixerStrip component for display-only track strips (no audio
routing). Mixer panel at bottom with drag resize handle (80-400px).
F7 keyboard shortcut, status bar toggle button, View menu item.
onUpdateTrack handles mute/solo/volume changes directly on tracks.
newId was defined inside updateActiveTracks callback but
referenced outside (line 15002 return). Moved declaration
before the callback as rearrangeNewId, accessible in both
the callback body and the outer return statement.
REARRANGE_TOOL_SPEC already has {type:'function', function:{...}}
wrapper. callLLM's tools.map(t => ({type:'function', function:t}))
added ANOTHER wrapper, creating nested:
{type:'function', function:{type:'function', function:{name}}}
which AI models couldn't parse -> function name became _unknown.
Fix: pass .function (inner spec) instead of the outer wrapper.
- showAIActionLog state defaults to false
- Action Log header clickable (cursor-pointer), shows '-' when
visible, '+' when hidden
- Body always rendered with CSS class 'hidden' toggled, avoids
deep ternary nesting that exceeded Babel 8 maxTargets limit
matchPreset was replacing systemInstruction with the preset's
template on keyword match. Typing 'epic orchestra' would
auto-apply 'Epic Orchestra Intro (8 Bars)' restrictive template.
Removed both override blocks (Piano Roll MIDI gen + DAW
rearrange). System instruction stays as generic default.
User can still fill prompt via suggestion click without
forced template override.
Backend sync effect (useEffect mount) was unconditionally
replacing mgr.presets with data.presets, destroying:
- Frontend-only default presets (12 vs backend's 7)
- User-defined presets in localStorage not yet synced to backend
Fix: merge backend presets by id — keep all local presets,
append any backend presets whose id doesn't exist locally.
User presets + frontend defaults are never lost.
Add 'Người dùng' category to filter dropdown. Filter logic
checks p.is_user_defined when this category is selected.
Shows only presets the user has created/added.
Use suggestions (filtered by getContextualSuggestions) instead
of promptMgr.presets (all). When MIDI item selected, only
Rearrange/Variation + favorites shown. When no MIDI item,
non-rearrange presets shown instead.
Replace horizontal suggestion chips (max 6/4) with vertical
scrollable list (max-h-36, no-scrollbar) showing ALL presets
from PromptTemplateManager (capped at 50). Header shows
'AI Suggestion' label. When MIDI item selected, MIDI item
name displayed in header. Each preset shows star/fav icon,
name, and category tag. Click fills the prompt textarea.
Keyboard handler useEffect has empty deps [], so selectedItemIds was
always captured as empty Set. Added selectedItemIdsRef to keep live
reference, matching pattern used by selectedClipIdRef.
- Ctrl+A already existed for selecting all timeline items across tracks
- Added Del/Backspace key handler for selectedItemIds: deletes all selected sections, MIDI items, and audio clips from the selected track(s)
- Clears selection after deletion and shows toast with count
Keyboard handler (useEffect [] deps) captures handleSaveSectionTab
from first render, which has sessionTabs = []. When section tab
is opened later, saved handleSaveSectionTab finds no tab and
returns silently. Added handleSaveSectionTabRef, updated on
every render, used in keyboard handler via ref.current.
Line 1163 called onSweepSelectStart which set up sweep
state (yellow selection rectangle). Mousemove then
selected all items in the sweep range. Changed to
onClearSelection = () => setSelectedItemIds(new Set())
which only clears selection without starting sweep.
First Ctrl+S handler (line 8066, handleExportSFS) catches
ALL Ctrl+S before second handler (line 8157, save section)
ever runs. Moved section-tab check into first handler:
activeTab.startsWith('session_') -> handleSaveSectionTab.
Main session Ctrl+S still exports SF (same as before).
Line 7942 checked activeTabRef.current !== 'main', which
is true for SECTION-TAB (session_xxx). Sub-tab block
only handles audio/image keys then unconditionally
returns at line 8005 -> Ctrl+A/S never reached.
Changed to exclude section tabs via
!activeTabRef.current.startsWith('session_').
Ctrl+S in SECTION-TAB (activeTab starts with session_):
calls handleSaveSectionTab(curTab).
Ctrl+A in MAIN/SECTION-TAB: collects sections, midiItems,
clips from activeTracksRef.current into selectedItemIds.
Ignores sub-tab contexts (PIANO_ROLL).
JSX rendering menu at line 17500 lacked the
!sessionTabs.some(s => s.id === activeTab) guard.
Added it - section tab context menu no longer offers
Insert Section (sections cannot contain section items).
_playNoteFluid called program_change synchronously at
call time, not at scheduled note time. startTrackPlayback
processes all MIDI items first, then all sections -> each
note's program_change overwritten by the last type processed.
All notes played with section instrument regardless of
actual time position. Moved program_change into doNote
(setTimeout callback) so it fires at correct time.
section cloned tracks have instrumentProgram:undefined.
Line 10283 defaulted to 0 -> FluidSynth program_change(ch,0)
-> plays whatever SoundFont is loaded (MAIN SF).
Added _isSectionClone flag; line 10283 skips program default
for section clones -> undefined -> _playNoteFluid silent return.
_playNoteFluid no-instrument branch previously called
_playNoteFallback (sine wave oscillator). Changed to
silent return. User explicitly requested no default sound.
Line 10283 defaulted to undefined when instrumentProgram
undefined, breaking Play for MAIN tracks with MIDI items
but no instrument. Restored: undefined ? instrumentProgram : 0.
Section sub-track (line 10393) keeps undefined -> oscillator
fallback to avoid loading MAIN SoundFont.
Revert two changes from 5d31563:
1. Explicit requestAnimationFrame in handlePlayPause (redundant,
useEffect already handles RAF start after setIsPlaying)
2. setCurrentTime(0) in handleEditSectionInTab (resets global
currentTime, interferes with MAIN session position)
Two fixes:
1. handlePlayPause: add explicit requestAnimationFrame(updatePlayhead)
after setIsPlaying(true) to ensure RAF starts immediately.
2. handleEditSectionInTab: add setCurrentTime(0) to reset playhead
to section start. Without this, currentTime may be outside
the section's timeline range (e.g. from main session playback).
Line 10283 same issue as 10393: when instrumentProgram is
undefined (section cloned tracks), program defaults to 0.
_playNoteFluid calls program_change(ch,0) on FluidSynth,
which loads program 0 from whatever SoundFont is loaded
(MAIN session's SF). Fix: pass undefined -> oscillator
fallback for all tracks without assigned instrument.
Line 10393 defaulted program to 0 when subTrack.instrumentProgram
was undefined (section clone with no instrument). _playNoteFluid
receives program=0, calls program_change(ch,0) on FluidSynth,
which picks program 0 from whatever SoundFont is loaded (MAIN
session's SF). Fix: pass undefined -> _playNoteFluid takes
oscillator fallback, no SoundFont loading.