From ee341154234cb82e5a5578460d8700af442c0b72 Mon Sep 17 00:00:00 2001 From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com> Date: Tue, 12 Aug 2025 18:04:14 -0700 Subject: [PATCH 01/14] auto-select new created track; delete selected track will change selection to an remaining track. --- src/components/track/TrackInfoItem.tsx | 6 ++-- src/stores/projectStore.ts | 43 +++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/components/track/TrackInfoItem.tsx b/src/components/track/TrackInfoItem.tsx index d697fe2..60b156a 100644 --- a/src/components/track/TrackInfoItem.tsx +++ b/src/components/track/TrackInfoItem.tsx @@ -214,9 +214,9 @@ const TrackInfoItem: React.FC = ({ } // Clear selection if this track is selected - if (selectedTrackId === track.getId().toString()) { - setSelectedTrack(null); - } + // if (selectedTrackId === track.getId().toString()) { + // setSelectedTrack(null); + // } // Delete the track using the command system await removeTrack(track.getId()); diff --git a/src/stores/projectStore.ts b/src/stores/projectStore.ts index 470fe43..fca7cd4 100644 --- a/src/stores/projectStore.ts +++ b/src/stores/projectStore.ts @@ -280,6 +280,14 @@ export const useProjectStore = create((set, get) => { const project = KGCore.instance().getCurrentProject(); set({ tracks: [...project.getTracks()] as KGTrack[] }); + // Auto-select the newly created track and open instrument selection panel + const newTrackId = command.getTrackId().toString(); + set({ + selectedTrackId: newTrackId, + showInstrumentSelection: true, + instrumentSelectionTrackId: newTrackId + }); + console.log(`Added track ${command.getTrackId()}`); } catch (error) { console.error('Error adding track:', error); @@ -289,13 +297,46 @@ export const useProjectStore = create((set, get) => { removeTrack: async (id: number) => { try { + // Get the current tracks and find the index of the track being deleted + const currentTracks = KGCore.instance().getCurrentProject().getTracks(); + const deletedTrackIndex = currentTracks.findIndex(track => track.getId() === id); + const currentSelectedTrackId = get().selectedTrackId; + const isCurrentTrackSelected = currentSelectedTrackId === id.toString(); + // Create and execute the remove track command const command = new RemoveTrackCommand(id); KGCore.instance().executeCommand(command); // Update the store state with a new array reference to trigger re-render const project = KGCore.instance().getCurrentProject(); - set({ tracks: [...project.getTracks()] as KGTrack[] }); + const remainingTracks = [...project.getTracks()] as KGTrack[]; + set({ tracks: remainingTracks }); + + // Auto-select another track if any remain + if (remainingTracks.length > 0) { + // Prefer previous track, fallback to next track + const newSelectedIndex = deletedTrackIndex > 0 + ? deletedTrackIndex - 1 // Select previous track + : 0; // Select first remaining track (was next) + + const newSelectedTrack = remainingTracks[newSelectedIndex]; + const newSelectedTrackId = newSelectedTrack.getId().toString(); + + setTimeout(() => { + set({ + selectedTrackId: isCurrentTrackSelected ? newSelectedTrackId : currentSelectedTrackId, + showInstrumentSelection: true, + instrumentSelectionTrackId: isCurrentTrackSelected ? newSelectedTrackId : currentSelectedTrackId, + }); + }, 0); + } else { + // No tracks left, clear selection and close instrument panel + set({ + selectedTrackId: null, + showInstrumentSelection: false, + instrumentSelectionTrackId: null + }); + } console.log(`Removed track ${id}`); } catch (error) { From 7ada648d1457a1c6b2faf459d3a742e2fc791621 Mon Sep 17 00:00:00 2001 From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com> Date: Tue, 12 Aug 2025 18:14:25 -0700 Subject: [PATCH 02/14] removed `instrumentSelectionTrackId` from projectStore, align the `instrumentSelectionTrackId` with `selectedTrackId`. --- src/components/InstrumentSelection.tsx | 8 +++--- src/components/track/TrackInfoItem.tsx | 4 +-- src/stores/projectStore.ts | 35 ++++++++------------------ 3 files changed, 17 insertions(+), 30 deletions(-) diff --git a/src/components/InstrumentSelection.tsx b/src/components/InstrumentSelection.tsx index 6d98609..e32ace2 100644 --- a/src/components/InstrumentSelection.tsx +++ b/src/components/InstrumentSelection.tsx @@ -6,14 +6,14 @@ import { KGMidiTrack, type InstrumentType } from '../core/track/KGMidiTrack'; const InstrumentSelection: React.FC = () => { const { tracks, - instrumentSelectionTrackId, + selectedTrackId, closeInstrumentSelection, setTrackInstrument } = useProjectStore(); const targetTrack = useMemo(() => { - return tracks.find(t => t.getId().toString() === instrumentSelectionTrackId) || null; - }, [tracks, instrumentSelectionTrackId]); + return tracks.find(t => t.getId().toString() === selectedTrackId) || null; + }, [tracks, selectedTrackId]); const currentInstrumentKey: InstrumentType = (targetTrack && targetTrack instanceof KGMidiTrack) ? (targetTrack.getInstrument() as InstrumentType) @@ -27,7 +27,7 @@ const InstrumentSelection: React.FC = () => { useEffect(() => { // Sync when the target track or its instrument changes setSelectedGroupKey(currentInstrumentDef?.group || 'PIANO_AND_KEYBOARDS'); - }, [instrumentSelectionTrackId, currentInstrumentKey, currentInstrumentDef]); + }, [selectedTrackId, currentInstrumentKey, currentInstrumentDef]); const groups = useMemo(() => Object.entries(INSTRUMENT_GROUPS) as Array<[string, string]>, []); diff --git a/src/components/track/TrackInfoItem.tsx b/src/components/track/TrackInfoItem.tsx index 60b156a..19b898a 100644 --- a/src/components/track/TrackInfoItem.tsx +++ b/src/components/track/TrackInfoItem.tsx @@ -193,8 +193,8 @@ const TrackInfoItem: React.FC = ({ e.stopPropagation(); // Select this track as active when opening instrument panel setSelectedTrack(track.getId().toString()); - // Toggle global InstrumentSelection panel for this track - toggleInstrumentSelectionForTrack(track.getId().toString()); + // Toggle global InstrumentSelection panel (it follows selectedTrackId) + toggleInstrumentSelectionForTrack(); }; // Handle settings button click diff --git a/src/stores/projectStore.ts b/src/stores/projectStore.ts index fca7cd4..26906d9 100644 --- a/src/stores/projectStore.ts +++ b/src/stores/projectStore.ts @@ -57,7 +57,7 @@ interface ProjectState { // Instrument selection panel state showInstrumentSelection: boolean; - instrumentSelectionTrackId: string | null; + // instrumentSelectionTrackId removed; panel now follows selectedTrackId // Settings state showSettings: boolean; @@ -105,8 +105,8 @@ interface ProjectState { toggleChatBox: () => void; // Instrument selection panel actions - openInstrumentSelectionForTrack: (trackId: string) => void; - toggleInstrumentSelectionForTrack: (trackId: string) => void; + openInstrumentSelectionForTrack: () => void; + toggleInstrumentSelectionForTrack: () => void; closeInstrumentSelection: () => void; // Settings actions @@ -200,7 +200,6 @@ export const useProjectStore = create((set, get) => { // Also auto-select it and open instrument selection panel let initialSelectedTrackId: string | null = null; let initialShowInstrumentSelection = false; - let initialInstrumentSelectionTrackId: string | null = null; try { const project = KGCore.instance().getCurrentProject(); if (project.getTracks().length === 0) { @@ -209,7 +208,6 @@ export const useProjectStore = create((set, get) => { const createdId = String(addDefaultTrackCommand.getTrackId()); initialSelectedTrackId = createdId; initialShowInstrumentSelection = true; - initialInstrumentSelectionTrackId = createdId; } } catch (error) { console.error('Error creating default track on startup:', error); @@ -242,7 +240,6 @@ export const useProjectStore = create((set, get) => { // Initial Instrument Selection panel state showInstrumentSelection: initialShowInstrumentSelection, - instrumentSelectionTrackId: initialInstrumentSelectionTrackId, // Initial Settings state showSettings: false, @@ -285,7 +282,6 @@ export const useProjectStore = create((set, get) => { set({ selectedTrackId: newTrackId, showInstrumentSelection: true, - instrumentSelectionTrackId: newTrackId }); console.log(`Added track ${command.getTrackId()}`); @@ -300,8 +296,8 @@ export const useProjectStore = create((set, get) => { // Get the current tracks and find the index of the track being deleted const currentTracks = KGCore.instance().getCurrentProject().getTracks(); const deletedTrackIndex = currentTracks.findIndex(track => track.getId() === id); - const currentSelectedTrackId = get().selectedTrackId; - const isCurrentTrackSelected = currentSelectedTrackId === id.toString(); + const { selectedTrackId } = get(); + const isCurrentTrackSelected = selectedTrackId === id.toString(); // Create and execute the remove track command const command = new RemoveTrackCommand(id); @@ -324,9 +320,7 @@ export const useProjectStore = create((set, get) => { setTimeout(() => { set({ - selectedTrackId: isCurrentTrackSelected ? newSelectedTrackId : currentSelectedTrackId, - showInstrumentSelection: true, - instrumentSelectionTrackId: isCurrentTrackSelected ? newSelectedTrackId : currentSelectedTrackId, + selectedTrackId: isCurrentTrackSelected ? newSelectedTrackId : selectedTrackId, }); }, 0); } else { @@ -334,7 +328,6 @@ export const useProjectStore = create((set, get) => { set({ selectedTrackId: null, showInstrumentSelection: false, - instrumentSelectionTrackId: null }); } @@ -533,7 +526,6 @@ export const useProjectStore = create((set, get) => { set({ selectedTrackId: firstTrackIdStr, showInstrumentSelection: true, - instrumentSelectionTrackId: firstTrackIdStr }); } @@ -644,13 +636,8 @@ export const useProjectStore = create((set, get) => { }, setSelectedTrack: (trackId: string | null) => { - const { showInstrumentSelection } = get(); // Update selected track id set({ selectedTrackId: trackId }); - // If instrument panel is open and a track is selected, retarget the panel - if (showInstrumentSelection && trackId) { - set({ instrumentSelectionTrackId: trackId }); - } }, // Piano roll actions @@ -688,14 +675,14 @@ export const useProjectStore = create((set, get) => { }, // Instrument selection panel actions - openInstrumentSelectionForTrack: (trackId: string) => { - set({ showInstrumentSelection: true, instrumentSelectionTrackId: trackId }); + openInstrumentSelectionForTrack: () => { + set({ showInstrumentSelection: true }); }, - toggleInstrumentSelectionForTrack: (trackId: string) => { - set({ showInstrumentSelection: true, instrumentSelectionTrackId: trackId }); + toggleInstrumentSelectionForTrack: () => { + set({ showInstrumentSelection: true }); }, closeInstrumentSelection: () => { - set({ showInstrumentSelection: false, instrumentSelectionTrackId: null }); + set({ showInstrumentSelection: false }); }, // Settings action implementations From 5e5a68e1d7fc5498946c79ddeb88e5a5bb3a3462 Mon Sep 17 00:00:00 2001 From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com> Date: Tue, 12 Aug 2025 18:25:00 -0700 Subject: [PATCH 03/14] added a treatment for InstrumentSelection panel when `selectedTrackId` is empty or doesn't associate with a valid track. --- src/components/InstrumentSelection.tsx | 30 ++++++++++++++------------ 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/components/InstrumentSelection.tsx b/src/components/InstrumentSelection.tsx index e32ace2..454a60c 100644 --- a/src/components/InstrumentSelection.tsx +++ b/src/components/InstrumentSelection.tsx @@ -42,8 +42,9 @@ const InstrumentSelection: React.FC = () => { }; const handleSelectInstrument = async (instrumentKey: string) => { - const instrument = instrumentKey as InstrumentType; + // If no valid target track, ignore user interaction if (!targetTrack || !(targetTrack instanceof KGMidiTrack)) return; + const instrument = instrumentKey as InstrumentType; try { await setTrackInstrument(targetTrack.getId(), instrument); } catch (err) { @@ -53,25 +54,26 @@ const InstrumentSelection: React.FC = () => { const previewImage = FLUIDR3_INSTRUMENT_MAP[currentInstrumentKey]?.image || 'piano.png'; const previewAlt = FLUIDR3_INSTRUMENT_MAP[currentInstrumentKey]?.displayName || currentInstrumentKey; - - if (!targetTrack) return null; + const hasTargetTrack = !!targetTrack; return ( -
+
-

{`${previewAlt.toString()}`}

+

{hasTargetTrack ? `${previewAlt.toString()}` : ''}

-
- {previewAlt.toString()} -
-
{targetTrack.getName()}
+ {hasTargetTrack && ( +
+ {previewAlt.toString()} +
+ )} +
{hasTargetTrack ? targetTrack.getName() : ''}
From 4fc2d773fd52ba090fe53e05a822fcb7212471b6 Mon Sep 17 00:00:00 2001 From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com> Date: Tue, 12 Aug 2025 18:56:55 -0700 Subject: [PATCH 04/14] when exporting KGProject to ABC Notation Music Sheet, we need to export all the tracks including the track contains user selected region, but we should skip the track with no region or no note. --- src/agent/tools/ReadMusicTool.ts | 88 +++++++++++--------------------- 1 file changed, 31 insertions(+), 57 deletions(-) diff --git a/src/agent/tools/ReadMusicTool.ts b/src/agent/tools/ReadMusicTool.ts index 3f27738..97e4658 100644 --- a/src/agent/tools/ReadMusicTool.ts +++ b/src/agent/tools/ReadMusicTool.ts @@ -4,9 +4,6 @@ import { KGMidiTrack } from '../../core/track/KGMidiTrack'; import { KGMidiRegion } from '../../core/region/KGMidiRegion'; import { convertRegionToABCNotation } from '../../util/abcNotationUtil'; import { KEY_SIGNATURE_MAP } from '../../constants/coreConstants'; -import { useProjectStore } from '../../stores/projectStore'; -import { KGRegion } from '../../core/region/KGRegion'; -import { KGCore } from '../../core/KGCore'; /** * Tool for reading music content from the project @@ -104,65 +101,43 @@ export class ReadMusicTool extends BaseTool { } /** - * Get KGCore instance + * Find tracks that should be skipped because they have no musical content + * (no regions or regions with no notes) */ - private getKGCore(): KGCore { - return KGCore.instance(); - } - - /** - * Find the track that contains the active piano roll region or first selected region - */ - private findTrackToSkip(tracks: KGMidiTrack[]): KGMidiTrack | null { + private findTracksToSkip(tracks: KGMidiTrack[]): KGMidiTrack[] { try { - const store = useProjectStore.getState(); - const core = this.getKGCore(); + const tracksToSkip: KGMidiTrack[] = []; - // First check for active piano roll region - if (store.activeRegionId) { - const activeRegion = this.findRegionById(store.activeRegionId, tracks); - if (activeRegion) { - const track = this.findTrackByRegion(activeRegion, tracks); - return track; + for (const track of tracks) { + const regions = track.getRegions(); + + // Skip tracks with no regions + if (regions.length === 0) { + tracksToSkip.push(track); + continue; + } + + // Check if all regions in this track are empty (have no notes) + const hasAnyNotes = regions.some(region => { + if (region.getCurrentType() === 'KGMidiRegion') { + return (region as KGMidiRegion).getNotes().length > 0; + } + return false; + }); + + // Skip tracks where no regions have notes + if (!hasAnyNotes) { + tracksToSkip.push(track); } } - // Then check for selected regions - const selectedItems = core.getSelectedItems(); - const selectedRegion = selectedItems.find((item: unknown) => item instanceof KGRegion) as KGRegion; - - if (selectedRegion) { - const track = this.findTrackByRegion(selectedRegion, tracks); - return track; - } - - return null; + return tracksToSkip; } catch (error) { - console.error('Error finding track to skip:', error); - return null; + console.error('Error finding tracks to skip:', error); + return []; } } - /** - * Find a region by ID across all tracks - */ - private findRegionById(regionId: string, tracks: KGMidiTrack[]): KGRegion | null { - for (const track of tracks) { - const regions = track.getRegions(); - const region = regions.find(r => r.getId() === regionId); - if (region) { - return region; - } - } - return null; - } - - /** - * Find track that contains the given region - */ - private findTrackByRegion(region: KGRegion, tracks: KGMidiTrack[]): KGMidiTrack | null { - return tracks.find(track => track.getRegions().includes(region)) || null; - } /** * Generate ABC notation for all tracks @@ -174,9 +149,8 @@ export class ReadMusicTool extends BaseTool { return 'No MIDI tracks found in the project.'; } - // Find the track to skip (unless it's the first track) - const trackToSkip = this.findTrackToSkip(midiTracks); - const firstTrack = midiTracks[0]; // The melody track + // Find tracks to skip (tracks with no content) + const tracksToSkip = this.findTracksToSkip(midiTracks); // Get project settings for proper notation const project = this.getCurrentProject(); @@ -187,8 +161,8 @@ export class ReadMusicTool extends BaseTool { let output = `All Tracks (beats ${startBeat}-${endBeat || 'end'}):\n\n`; midiTracks.forEach((track, index) => { - // Skip this track if it's the track to skip AND it's not the first track (melody) - if (trackToSkip && track === trackToSkip && track !== firstTrack) { + // Skip tracks that have no musical content + if (tracksToSkip.includes(track)) { return; // Skip this track } const trackNumber = index + 1; From f9fc309c913b251aed4b09d99e5d426005fc67ce Mon Sep 17 00:00:00 2001 From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com> Date: Tue, 12 Aug 2025 19:05:18 -0700 Subject: [PATCH 05/14] updated `error_no_selected_region.md`. --- public/chat/error_no_selected_region.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/public/chat/error_no_selected_region.md b/public/chat/error_no_selected_region.md index a0b524e..5b4e1f5 100644 --- a/public/chat/error_no_selected_region.md +++ b/public/chat/error_no_selected_region.md @@ -1,10 +1,14 @@ -### No Region Selected +### No MIDI Region Selected -To proceed, please select a MIDI region in the track view or open a region in the piano roll. +Please select a MIDI region in your track to continue. -Tips: -- Double-click in a track to create a new MIDI region, then select it. -- Click an existing region to select it. -- Use the Piano button to open the active region in the piano roll. +The K.G.Studio Musician Assistant Agent can only add, remove, or edit notes within the boundaries of the selected region. Ensure your region's start and end beats covers the musical section you want to work on. The agent will focus its edits within this region, though it may reference music outside the selection if needed for context. -After selecting a region, try your request again. \ No newline at end of file +When the agent is not working well, try to reduce the region size to limit the scope of the agent's work. + +**Region Operations:** +- Double-click in a track to create and select a new MIDI region. +- Click on an existing region to select it. +- Use the Piano button to open and work with the currently active region in the piano roll. + +After selecting a region, please try your request again. \ No newline at end of file From 3534dc7fca63c8629aa974d6dd6cb7750e09afa1 Mon Sep 17 00:00:00 2001 From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com> Date: Tue, 12 Aug 2025 19:29:32 -0700 Subject: [PATCH 06/14] hide the ChatBox instead of unmount it to prevent lose chat history. --- src/App.tsx | 7 +++--- src/components/ChatBox.tsx | 51 ++++++++++++++++++++++++++------------ src/util/chatUtil.ts | 4 +-- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 2567e69..3dfe826 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -61,15 +61,14 @@ function App() { {/* Main Display Area containing MainContent, ChatBox, and Settings */}
- {showSettings ? ( - setShowSettings(false)} /> - ) : ( + {showSettings && setShowSettings(false)} />} + {!showSettings && ( <> {showInstrumentSelection && } - {showChatBox && } )} +
{/* Track Control */} diff --git a/src/components/ChatBox.tsx b/src/components/ChatBox.tsx index fd389a9..9734855 100644 --- a/src/components/ChatBox.tsx +++ b/src/components/ChatBox.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect, memo } from 'react'; +import React, { useState, useRef, useEffect, memo, useCallback } from 'react'; import { FaPlus, FaBan } from 'react-icons/fa'; import { UserMessage, AssistantMessage } from './chat'; import { AgentCore } from '../agent/core/AgentCore'; @@ -44,7 +44,11 @@ const createLLMProvider = (): LLMProvider => { } }; -const ChatBox: React.FC = () => { +interface ChatBoxProps { + isVisible: boolean; +} + +const ChatBox: React.FC = ({ isVisible }) => { const [inputValue, setInputValue] = useState(''); const textareaRef = useRef(null); @@ -62,6 +66,33 @@ const ChatBox: React.FC = () => { // Track if this is the first message (for system prompt logging) const [isFirstMessage, setIsFirstMessage] = useState(true); + const generateMessageId = (): string => { + return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; + }; + + const clearChatUI = useCallback(async () => { + // Clear UI state + setMessages([]); + + // Reset first message flag so system prompt will be logged again + setIsFirstMessage(true); + + // Auto-show welcome message after clearing (like on app startup) + try { + const result = await processUserMessage('/welcome'); + if (result.pseudoAssistantResponse) { + const pseudoId = generateMessageId(); + setMessages(prev => [...prev, { + id: pseudoId, + role: 'assistant', + content: result.pseudoAssistantResponse!, + }]); + } + } catch { + // ignore errors, just don't show welcome if it fails + } + }, []); + // Initialize AgentCore with configured provider and register clear UI callback useEffect(() => { const initializeProvider = async () => { @@ -103,11 +134,7 @@ const ChatBox: React.FC = () => { // ignore } })(); - }, []); - - const generateMessageId = (): string => { - return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; - }; + }, [clearChatUI]); const handleAbort = () => { if (abortController) { @@ -128,14 +155,6 @@ const ChatBox: React.FC = () => { } }; - const clearChatUI = () => { - // Clear UI state - setMessages([]); - - // Reset first message flag so system prompt will be logged again - setIsFirstMessage(true); - }; - const handleClearCommand = () => { const { setStatus } = useProjectStore.getState(); clearChatHistoryAndUI(setStatus); @@ -519,7 +538,7 @@ const ChatBox: React.FC = () => { }, [isProcessing, isExecutingTools]); return ( -
+

K.G.Studio Musician Assistant

diff --git a/src/util/chatUtil.ts b/src/util/chatUtil.ts index 3e8380e..1553417 100644 --- a/src/util/chatUtil.ts +++ b/src/util/chatUtil.ts @@ -25,13 +25,13 @@ export const clearChatHistoryWithStatus = (setStatus?: (message: string) => void }; // Global callback for clearing UI state -let globalClearChatUI: (() => void) | null = null; +let globalClearChatUI: (() => void | Promise) | null = null; /** * Register a callback to clear chat UI state * This allows external components to clear the ChatBox UI */ -export const registerClearChatUICallback = (callback: () => void) => { +export const registerClearChatUICallback = (callback: () => void | Promise) => { globalClearChatUI = callback; }; From cac9d47f3dfbd97a50df98d06af18e780b1db4d3 Mon Sep 17 00:00:00 2001 From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com> Date: Tue, 12 Aug 2025 19:47:57 -0700 Subject: [PATCH 07/14] updated system prompt to emphasize when adding notes, it should NOT omit any notes. --- public/prompts/system.md | 1 + 1 file changed, 1 insertion(+) diff --git a/public/prompts/system.md b/public/prompts/system.md index 85c30c0..e5db53e 100644 --- a/public/prompts/system.md +++ b/public/prompts/system.md @@ -222,6 +222,7 @@ You have access to two tools for working with the current music region: **remove ## Important Considerations +- **Do not omit notes**: It is important that when adding notes, you must explicitly output every note that should be added — do not omit, summarize, or replace them with comments like “...”. Even if the pattern is repetitive, list all notes in full detail in the correct order. NEVER OMIT ANY NOTES IN THE XML BECAUSE OF REPETITION!! - **Reading Music**: You should NEVER ask the user to manually provide you music pieces BEFORE invoking the read_music tool. Always use the read_music tool to get the music pieces first. - **Music Validation**: Always validate your musical choices: - Ensure pitches are within reasonable ranges for the current instrument From e2e992cbdaf08e6f70359962f4aa0b1f285d2339 Mon Sep 17 00:00:00 2001 From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com> Date: Tue, 12 Aug 2025 20:01:51 -0700 Subject: [PATCH 08/14] when converting KGTrack to ABC Notation Music Sheet, we should display the drum instrument name instead of the real track name in case the real track name can't indicate the track is using drum pitch notes. --- src/agent/tools/ReadMusicTool.ts | 38 ++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/agent/tools/ReadMusicTool.ts b/src/agent/tools/ReadMusicTool.ts index 97e4658..cf39a80 100644 --- a/src/agent/tools/ReadMusicTool.ts +++ b/src/agent/tools/ReadMusicTool.ts @@ -4,6 +4,7 @@ import { KGMidiTrack } from '../../core/track/KGMidiTrack'; import { KGMidiRegion } from '../../core/region/KGMidiRegion'; import { convertRegionToABCNotation } from '../../util/abcNotationUtil'; import { KEY_SIGNATURE_MAP } from '../../constants/coreConstants'; +import { FLUIDR3_INSTRUMENT_MAP } from '../../constants/generalMidiConstants'; /** * Tool for reading music content from the project @@ -31,6 +32,25 @@ export class ReadMusicTool extends BaseTool { } }; + /** + * Get the display name for percussion instruments, or null if not percussion + */ + private getPercussionDisplayName(track: KGMidiTrack): string | null { + try { + const instrument = track.getInstrument(); + const instrumentInfo = FLUIDR3_INSTRUMENT_MAP[instrument]; + + if (instrumentInfo && instrumentInfo.group === 'PERCUSSION_KIT') { + return instrumentInfo.displayName; + } + + return null; + } catch (error) { + console.error('Error getting percussion display name:', error); + return null; + } + } + async execute(params: Record): Promise { try { // Validate parameters @@ -168,8 +188,22 @@ export class ReadMusicTool extends BaseTool { const trackNumber = index + 1; const trackName = track.getName() || `Track ${trackNumber}`; - // hardcode the 1st track to be the melody, other track names are the same as the original track names - output += `Track ${trackNumber} - ${trackNumber === 1 ? 'Melody' : trackName}:\n`; + // Check if this track uses a percussion instrument + const percussionDisplayName = this.getPercussionDisplayName(track); + + let displayTrackName: string; + if (percussionDisplayName) { + // Use percussion instrument display name for all percussion tracks + displayTrackName = percussionDisplayName; + } else if (trackNumber === 1) { + // Use "Melody" for the first non-percussion track + displayTrackName = 'Melody'; + } else { + // Use original track name for other non-percussion tracks + displayTrackName = trackName; + } + + output += `Track ${trackNumber} - ${displayTrackName}:\n`; // Get all regions from the track and convert each one const regions = track.getRegions().filter(region => region instanceof KGMidiRegion) as KGMidiRegion[]; From 585690a0469bce8d5ebe79210b7b95d3adb9ce0d Mon Sep 17 00:00:00 2001 From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com> Date: Tue, 12 Aug 2025 21:10:49 -0700 Subject: [PATCH 09/14] fixed the issue that changing instrument will cause track volume back to default. --- src/core/audio-interface/KGAudioBus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/audio-interface/KGAudioBus.ts b/src/core/audio-interface/KGAudioBus.ts index a85f986..e1a51db 100644 --- a/src/core/audio-interface/KGAudioBus.ts +++ b/src/core/audio-interface/KGAudioBus.ts @@ -215,7 +215,7 @@ export class KGAudioBus { this.instrument = newInstrument; // Restore volume settings - // this.updateSamplerVolume(); + this.updateSamplerVolume(); console.log(`Instrument changed successfully to ${newInstrument}`); } catch (error) { From 47872287cef5c6fb0e4ee5ff485071d05fc954f2 Mon Sep 17 00:00:00 2001 From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com> Date: Tue, 12 Aug 2025 21:18:36 -0700 Subject: [PATCH 10/14] prevent users from creating overlapping regions. --- src/components/track/TrackGridPanel.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/components/track/TrackGridPanel.tsx b/src/components/track/TrackGridPanel.tsx index 37b0bff..77c1348 100644 --- a/src/components/track/TrackGridPanel.tsx +++ b/src/components/track/TrackGridPanel.tsx @@ -71,6 +71,25 @@ const TrackGridPanel: React.FC = ({ // Get beats per bar from the time signature const beatsPerBar = timeSignature.numerator; + // Check for overlapping regions before creating a new one + const newRegionStartBeat = (barNumber - 1) * beatsPerBar; + const newRegionEndBeat = newRegionStartBeat + beatsPerBar - 1; + + const existingRegions = track.getRegions(); + const hasOverlap = existingRegions.some(region => { + const existingStart = region.getStartFromBeat(); + const existingEnd = existingStart + region.getLength() - 1; + return newRegionStartBeat <= existingEnd && newRegionEndBeat >= existingStart; + }); + + if (hasOverlap) { + if (DEBUG_MODE.TRACK_GRID_PANEL) { + console.log(`Cannot create region at bar ${barNumber}: overlaps with existing region`); + } + alert('Cannot create region: overlaps with existing region'); + return; // Don't create the region if it overlaps + } + // Create and execute the region creation command const command = CreateRegionCommand.fromBarCoordinates( trackId, From 2510014580e667664e30a3e1701a8a6d8e01add6 Mon Sep 17 00:00:00 2001 From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com> Date: Tue, 12 Aug 2025 21:41:11 -0700 Subject: [PATCH 11/14] updated the naming convention for new tracks and regions. New tracks now default to 'Track {i}', and new regions default to '{track_name} Region {i}'. --- src/components/track/TrackGridPanel.tsx | 3 +- src/core/commands/track/AddTrackCommand.ts | 3 +- src/util/miscUtil.ts | 54 ++++++++++++++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/components/track/TrackGridPanel.tsx b/src/components/track/TrackGridPanel.tsx index 77c1348..50b104e 100644 --- a/src/components/track/TrackGridPanel.tsx +++ b/src/components/track/TrackGridPanel.tsx @@ -9,6 +9,7 @@ import { KGMainContentState } from '../../core/state/KGMainContentState'; import { isModifierKeyPressed } from '../../util/osUtil'; import { CreateRegionCommand, ResizeRegionCommand, MoveRegionCommand } from '../../core/commands'; import { KGCore } from '../../core/KGCore'; +import { generateNewRegionName } from '../../util/miscUtil'; interface TrackGridPanelProps { tracks: KGTrack[]; @@ -97,7 +98,7 @@ const TrackGridPanel: React.FC = ({ barNumber, 1, // Default to 1 bar length beatsPerBar, - `${track.getName()} Region` + generateNewRegionName(trackId) ); KGCore.instance().executeCommand(command); diff --git a/src/core/commands/track/AddTrackCommand.ts b/src/core/commands/track/AddTrackCommand.ts index bf1a744..fb33a85 100644 --- a/src/core/commands/track/AddTrackCommand.ts +++ b/src/core/commands/track/AddTrackCommand.ts @@ -2,6 +2,7 @@ import { KGCommand } from '../KGCommand'; import { KGCore } from '../../KGCore'; import { KGMidiTrack, type InstrumentType } from '../../track/KGMidiTrack'; import { KGAudioInterface } from '../../audio-interface/KGAudioInterface'; +import { generateNewTrackName } from '../../../util/miscUtil'; /** * Command to add a new track to the project @@ -28,7 +29,7 @@ export class AddTrackCommand extends KGCommand { this.trackId = trackId; } - this.trackName = trackName || `Track ${this.trackId}`; + this.trackName = trackName || generateNewTrackName(); this.instrument = instrument; // Track index will be set during execution diff --git a/src/util/miscUtil.ts b/src/util/miscUtil.ts index 730dd1c..6784e17 100644 --- a/src/util/miscUtil.ts +++ b/src/util/miscUtil.ts @@ -2,6 +2,8 @@ * Miscellaneous utility functions */ +import { KGCore } from '../core/KGCore'; + /** * Generates a unique ID with a consistent format * @param prefix - The prefix for the ID (typically class name like 'KGMidiNote') @@ -12,4 +14,56 @@ export const generateUniqueId = (prefix: string): string => { const timestamp = Date.now(); const randomString = Math.random().toString(36).substring(2, 11); // 9 character random string return `${prefix}_${timestamp}_${randomString}`; +}; + +/** + * Generates a new sequential track name that doesn't conflict with existing tracks + * @returns A track name in format "Track {number}" where number is the next available sequential number + * @example generateNewTrackName() -> 'Track 1' (if no tracks exist) + * @example generateNewTrackName() -> 'Track 3' (if 'Track 1' and 'Track 2' already exist) + */ +export const generateNewTrackName = (): string => { + const currentProject = KGCore.instance().getCurrentProject(); + const existingTracks = currentProject.getTracks(); + const existingNames = existingTracks.map(track => track.getName()); + + let i = 1; + while (true) { + const candidateName = `Track ${i}`; + if (!existingNames.includes(candidateName)) { + return candidateName; + } + i++; + } +}; + +/** + * Generates a new sequential region name that doesn't conflict with existing regions on the same track + * @param trackId - The ID of the track where the region will be created + * @returns A region name in format "{trackName} Region {number}" where number is the next available sequential number + * @example generateNewRegionName('1') -> 'Piano Region 1' (if no regions exist on track) + * @example generateNewRegionName('1') -> 'Piano Region 3' (if 'Piano Region 1' and 'Piano Region 2' already exist) + */ +export const generateNewRegionName = (trackId: string): string => { + const currentProject = KGCore.instance().getCurrentProject(); + const tracks = currentProject.getTracks(); + const targetTrack = tracks.find(track => track.getId().toString() === trackId); + + if (!targetTrack) { + // Fallback if track not found + return 'Region 1'; + } + + const trackName = targetTrack.getName(); + const existingRegions = targetTrack.getRegions(); + const existingNames = existingRegions.map(region => region.getName()); + + let i = 1; + while (true) { + const candidateName = `${trackName} Region ${i}`; + if (!existingNames.includes(candidateName)) { + return candidateName; + } + i++; + } }; \ No newline at end of file From 91dc9e5023d0f1aff3eab092d54614c0a252d67c Mon Sep 17 00:00:00 2001 From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com> Date: Tue, 12 Aug 2025 23:02:53 -0700 Subject: [PATCH 12/14] the code has been updated to ensure that the latest configurations are always retrieved when making calls to the LLM. --- src/agent/llm/OpenAIProvider.ts | 64 ++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/src/agent/llm/OpenAIProvider.ts b/src/agent/llm/OpenAIProvider.ts index 8365c96..92f19e0 100644 --- a/src/agent/llm/OpenAIProvider.ts +++ b/src/agent/llm/OpenAIProvider.ts @@ -10,36 +10,38 @@ import { URL_CONSTANTS } from '../../constants/coreConstants'; export class OpenAIProvider extends LLMProvider { readonly name = 'OpenAI'; - private apiKey: string; - private model: string; - private flexMode: boolean = false; - private baseURL: string; - private isCompatibleProvider: boolean; - private apiEndpoint: string; private isOllamaFormat: boolean | null = null; // Detected at runtime constructor() { super(); - + } + + /** + * Get current configuration values from ConfigManager + */ + private getCurrentConfig() { const configManager = ConfigManager.instance(); const llmProvider = configManager.get('general.llm_provider') as string; - this.isCompatibleProvider = llmProvider === 'openai_compatible'; + const isCompatibleProvider = llmProvider === 'openai_compatible'; - // Set API key, model, base URL, and endpoint based on provider type - if (this.isCompatibleProvider) { - this.apiKey = configManager.get('general.openai_compatible.api_key') as string; - this.model = configManager.get('general.openai_compatible.model') as string; - this.baseURL = configManager.get('general.openai_compatible.base_url') as string; + if (isCompatibleProvider) { + const apiKey = configManager.get('general.openai_compatible.api_key') as string; + const model = configManager.get('general.openai_compatible.model') as string; + const baseURL = configManager.get('general.openai_compatible.base_url') as string; // For compatible providers, use the full URL as provided (assume it includes the endpoint) // Common patterns: http://localhost:11434/api/chat (Ollama), https://api.openrouter.ai/v1 (OpenRouter) - this.apiEndpoint = this.baseURL; - this.flexMode = false; // Not applicable to compatible providers + const apiEndpoint = baseURL; + const flexMode = false; // Not applicable to compatible providers + + return { apiKey, model, baseURL, apiEndpoint, flexMode, isCompatibleProvider }; } else { - this.apiKey = configManager.get('general.openai.api_key') as string; - this.model = configManager.get('general.openai.model') as string; - this.flexMode = (configManager.get('general.openai.flex') as boolean) === true; - this.baseURL = URL_CONSTANTS.DEFAULT_OPENAI_BASE_URL; - this.apiEndpoint = `${this.baseURL}/chat/completions`; + const apiKey = configManager.get('general.openai.api_key') as string; + const model = configManager.get('general.openai.model') as string; + const flexMode = (configManager.get('general.openai.flex') as boolean) === true; + const baseURL = URL_CONSTANTS.DEFAULT_OPENAI_BASE_URL; + const apiEndpoint = `${baseURL}/chat/completions`; + + return { apiKey, model, baseURL, apiEndpoint, flexMode, isCompatibleProvider }; } } @@ -127,6 +129,9 @@ export class OpenAIProvider extends LLMProvider { systemPrompt?: string, tools?: Record[] ): AsyncIterableIterator { + // Get fresh config values + const config = this.getCurrentConfig(); + // Build OpenAI messages array with role preservation const openAIMessages: Array<{ role: string; content: string }> = []; @@ -141,15 +146,15 @@ export class OpenAIProvider extends LLMProvider { content: msg.content }))); - const response = await fetch(this.apiEndpoint, { + const response = await fetch(config.apiEndpoint, { method: 'POST', headers: { - 'Authorization': `Bearer ${this.apiKey}`, + 'Authorization': `Bearer ${config.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ - model: this.model, - ...(this.flexMode && !this.isCompatibleProvider ? { service_tier: 'flex' } : {}), + model: config.model, + ...(config.flexMode && !config.isCompatibleProvider ? { service_tier: 'flex' } : {}), messages: openAIMessages, stream: true, tools: tools || undefined @@ -332,6 +337,9 @@ export class OpenAIProvider extends LLMProvider { systemPrompt?: string, tools?: Record[] ): Promise { + // Get fresh config values + const config = this.getCurrentConfig(); + // Build OpenAI messages array with role preservation const openAIMessages: Array<{ role: string; content: string }> = []; @@ -346,15 +354,15 @@ export class OpenAIProvider extends LLMProvider { content: msg.content }))); - const response = await fetch(this.apiEndpoint, { + const response = await fetch(config.apiEndpoint, { method: 'POST', headers: { - 'Authorization': `Bearer ${this.apiKey}`, + 'Authorization': `Bearer ${config.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ - model: this.model, - ...(this.flexMode && !this.isCompatibleProvider ? { service_tier: 'flex' } : {}), + model: config.model, + ...(config.flexMode && !config.isCompatibleProvider ? { service_tier: 'flex' } : {}), messages: openAIMessages, stream: false, tools: tools || undefined From 4903450ff4d579296fa47576333f055e3f41c752 Mon Sep 17 00:00:00 2001 From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com> Date: Tue, 12 Aug 2025 23:04:07 -0700 Subject: [PATCH 13/14] updated custom instructions for the `qwen3-a3b-30b` model to address and prevent instances of inactivity or 'laziness' when generating notes for drum kit tracks. --- public/chat/custom_instructions_qwen3-a3b-30b.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/public/chat/custom_instructions_qwen3-a3b-30b.md b/public/chat/custom_instructions_qwen3-a3b-30b.md index 7e72b63..471a1a9 100644 --- a/public/chat/custom_instructions_qwen3-a3b-30b.md +++ b/public/chat/custom_instructions_qwen3-a3b-30b.md @@ -1 +1,7 @@ -When making chord progression for existing melody, you should be very careful to make sure the chords can match the notes in the melody at any given time, they should sound harmony. \ No newline at end of file +When making chord progression for existing melody, you should be very careful to make sure the chords can match the notes in the melody at any given time, they should sound harmony. + +DO NOT OMIT ANYTHING WHEN ADDING NOTES! +THINGS LIKE BELOW SHOULD NEVER HAPPEN!!! +``` + +``` From fc151c818f6e3f53243a267e10654961d38f77f2 Mon Sep 17 00:00:00 2001 From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com> Date: Tue, 12 Aug 2025 23:19:28 -0700 Subject: [PATCH 14/14] remove debug info from AttemptCompletionTool. --- src/agent/tools/AttemptCompletionTool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agent/tools/AttemptCompletionTool.ts b/src/agent/tools/AttemptCompletionTool.ts index be8da7d..8e2f271 100644 --- a/src/agent/tools/AttemptCompletionTool.ts +++ b/src/agent/tools/AttemptCompletionTool.ts @@ -39,7 +39,7 @@ export class AttemptCompletionTool extends BaseTool { agentState.setIsWorkingOnTask(false); return this.createSuccessResult( - `Task completed: ${comment}. Agent task status updated to not working.` + `Task completed: ${comment}. ` ); } catch (error) {