feat: added delete_track tool
This commit is contained in:
@@ -39,6 +39,9 @@ Create a new MIDI track using a `track_name` and an `instrument`. The `instrumen
|
|||||||
## update_track
|
## update_track
|
||||||
Update an existing MIDI track by `track_id` or `track_name`. Prefer `track_id` because `track_name` may be duplicated. You can rename the track with `new_track_name` and/or change the instrument with `instrument`. The `instrument` parameter must be the exact English instrument name returned by `list_all_available_instruments`.
|
Update an existing MIDI track by `track_id` or `track_name`. Prefer `track_id` because `track_name` may be duplicated. You can rename the track with `new_track_name` and/or change the instrument with `instrument`. The `instrument` parameter must be the exact English instrument name returned by `list_all_available_instruments`.
|
||||||
|
|
||||||
|
## delete_track
|
||||||
|
Delete an existing MIDI track by `track_id` or `track_name`. Prefer `track_id` because `track_name` may be duplicated. If multiple MIDI tracks share the same `track_name`, do not guess which one to delete; use `track_id`.
|
||||||
|
|
||||||
## read_chord_progression
|
## read_chord_progression
|
||||||
Read the user-defined chord progression from the global chord track. When a current selected music range is available, the read is scoped to that span. Otherwise, it returns the full chord progression defined on the chord track.
|
Read the user-defined chord progression from the global chord track. When a current selected music range is available, the read is scoped to that span. Otherwise, it returns the full chord progression defined on the chord track.
|
||||||
|
|
||||||
@@ -71,6 +74,7 @@ To create a melodic line, use sequential `start` values for each note. To create
|
|||||||
11. If you are editing notes for the currently selected track, you do not need to pass `track_id` or `track_name`; the editing tools can use the selected track context directly.
|
11. If you are editing notes for the currently selected track, you do not need to pass `track_id` or `track_name`; the editing tools can use the selected track context directly.
|
||||||
12. If a required parameter cannot be determined from context, ask the user instead of guessing.
|
12. If a required parameter cannot be determined from context, ask the user instead of guessing.
|
||||||
13. Proceed step-by-step. Each action should build on confirmed results from previous steps.
|
13. Proceed step-by-step. Each action should build on confirmed results from previous steps.
|
||||||
|
14. Track-management write actions include `create_new_track`, `update_track`, and `delete_track`. Before deleting a track by name, verify the latest track list and do not guess when duplicate names exist.
|
||||||
|
|
||||||
====
|
====
|
||||||
|
|
||||||
@@ -137,6 +141,7 @@ CAPABILITIES
|
|||||||
- **Selection Awareness**: Use `get_user_selected_music_range_and_track` to confirm the current selected music range and selected regular track whenever selection context is important to the task.
|
- **Selection Awareness**: Use `get_user_selected_music_range_and_track` to confirm the current selected music range and selected regular track whenever selection context is important to the task.
|
||||||
- **Track Awareness**: Use `list_all_tracks` to inspect all available MIDI tracks and their instruments before choosing a target track.
|
- **Track Awareness**: Use `list_all_tracks` to inspect all available MIDI tracks and their instruments before choosing a target track.
|
||||||
- **Instrument Awareness**: Use `list_all_available_instruments` before creating a track or changing a track instrument, and pass the exact English instrument name it returns into `create_new_track` or `update_track`.
|
- **Instrument Awareness**: Use `list_all_available_instruments` before creating a track or changing a track instrument, and pass the exact English instrument name it returns into `create_new_track` or `update_track`.
|
||||||
|
- **Track Management**: You can create, update, and delete MIDI tracks. Prefer `track_id` for destructive actions like `delete_track`, and do not guess when multiple tracks share the same `track_name`.
|
||||||
- **Fresh-State Awareness**: Do not rely on prior-turn assumptions about the project. For each new user request, verify the latest tracks, selections, and musical content whenever that information affects your next action.
|
- **Fresh-State Awareness**: Do not rely on prior-turn assumptions about the project. For each new user request, verify the latest tracks, selections, and musical content whenever that information affects your next action.
|
||||||
- **Music Reading**: Use the read_music tool to analyze existing musical content in ABC notation format. Multiple tracks will be presented separately, and track names (e.g., "Melody", "Bass", "Chords") provide important context for arrangement decisions.
|
- **Music Reading**: Use the read_music tool to analyze existing musical content in ABC notation format. Multiple tracks will be presented separately, and track names (e.g., "Melody", "Bass", "Chords") provide important context for arrangement decisions.
|
||||||
- **Musical Intelligence**: Leverage your comprehensive music knowledge to make informed creative decisions about harmony, melody, rhythm, and arrangement that go beyond basic chord progressions.
|
- **Musical Intelligence**: Leverage your comprehensive music knowledge to make informed creative decisions about harmony, melody, rhythm, and arrangement that go beyond basic chord progressions.
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { DeleteTrackTool } from './DeleteTrackTool';
|
||||||
|
import { KGCore } from '../../core/KGCore';
|
||||||
|
import { KGProject } from '../../core/KGProject';
|
||||||
|
import { KGMidiTrack } from '../../core/track/KGMidiTrack';
|
||||||
|
import { KGAudioTrack } from '../../core/track/KGAudioTrack';
|
||||||
|
import { KGAudioInterface } from '../../core/audio-interface/KGAudioInterface';
|
||||||
|
|
||||||
|
vi.mock('../../stores/projectStore', () => ({
|
||||||
|
useProjectStore: {
|
||||||
|
getState: () => ({
|
||||||
|
activeRegionId: null,
|
||||||
|
selectedRegionIds: [],
|
||||||
|
selectedTrackId: null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
function mockCore(project: KGProject) {
|
||||||
|
vi.spyOn(KGCore, 'instance').mockReturnValue({
|
||||||
|
getCurrentProject: () => project,
|
||||||
|
executeCommand: (command: { execute(): void }) => command.execute(),
|
||||||
|
} as unknown as KGCore);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('DeleteTrackTool', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.spyOn(KGAudioInterface, 'instance').mockReturnValue({
|
||||||
|
removeTrackSynth: vi.fn(),
|
||||||
|
removeTrackAudioPlayerBus: vi.fn(),
|
||||||
|
} as unknown as KGAudioInterface);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes a MIDI track by track_id', async () => {
|
||||||
|
const leadTrack = new KGMidiTrack('Lead', 1, 'trumpet');
|
||||||
|
const bassTrack = new KGMidiTrack('Bass', 2, 'acoustic_bass');
|
||||||
|
const project = new KGProject('delete-by-id-project');
|
||||||
|
project.setTracks([leadTrack, bassTrack]);
|
||||||
|
mockCore(project);
|
||||||
|
|
||||||
|
const tool = new DeleteTrackTool();
|
||||||
|
const result = await tool.execute({ track_id: '1' });
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
result: 'Track deleted:\ntrack_id: 1\ntrack_name: Lead',
|
||||||
|
});
|
||||||
|
expect(project.getTracks().map(track => track.getName())).toEqual(['Bass']);
|
||||||
|
expect(tool.buildToolResultDisplayContent({ track_id: '1' }, result)).toBe(
|
||||||
|
'Track deleted:\n- track_id: 1\n- track_name: Lead',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes a MIDI track by track_name', async () => {
|
||||||
|
const leadTrack = new KGMidiTrack('Lead', 1, 'trumpet');
|
||||||
|
const bassTrack = new KGMidiTrack('Bass', 2, 'acoustic_bass');
|
||||||
|
const project = new KGProject('delete-by-name-project');
|
||||||
|
project.setTracks([leadTrack, bassTrack]);
|
||||||
|
mockCore(project);
|
||||||
|
|
||||||
|
const tool = new DeleteTrackTool();
|
||||||
|
const result = await tool.execute({ track_name: 'Bass' });
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
result: 'Track deleted:\ntrack_id: 2\ntrack_name: Bass',
|
||||||
|
});
|
||||||
|
expect(project.getTracks().map(track => track.getName())).toEqual(['Lead']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses track_id when both track_id and track_name are provided', async () => {
|
||||||
|
const leadTrack = new KGMidiTrack('Lead', 1, 'trumpet');
|
||||||
|
const bassTrack = new KGMidiTrack('Bass', 2, 'acoustic_bass');
|
||||||
|
const project = new KGProject('delete-track-id-precedence-project');
|
||||||
|
project.setTracks([leadTrack, bassTrack]);
|
||||||
|
mockCore(project);
|
||||||
|
|
||||||
|
const tool = new DeleteTrackTool();
|
||||||
|
const result = await tool.execute({
|
||||||
|
track_id: '2',
|
||||||
|
track_name: 'Lead',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(project.getTracks().map(track => track.getName())).toEqual(['Lead']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects duplicate track names when track_id is omitted', async () => {
|
||||||
|
const firstLead = new KGMidiTrack('Lead', 1, 'trumpet');
|
||||||
|
const secondLead = new KGMidiTrack('Lead', 2, 'flute');
|
||||||
|
const project = new KGProject('delete-duplicate-name-project');
|
||||||
|
project.setTracks([firstLead, secondLead]);
|
||||||
|
mockCore(project);
|
||||||
|
|
||||||
|
const tool = new DeleteTrackTool();
|
||||||
|
const result = await tool.execute({ track_name: 'Lead' });
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
result: 'Multiple MIDI tracks share the name "Lead". Provide track_id instead.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors when neither identifier is provided', async () => {
|
||||||
|
const project = new KGProject('delete-missing-id-project');
|
||||||
|
mockCore(project);
|
||||||
|
|
||||||
|
const tool = new DeleteTrackTool();
|
||||||
|
const result = await tool.execute({});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
result: 'Either track_id or track_name must be provided.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors when the target track does not exist', async () => {
|
||||||
|
const project = new KGProject('delete-missing-track-project');
|
||||||
|
mockCore(project);
|
||||||
|
|
||||||
|
const tool = new DeleteTrackTool();
|
||||||
|
const result = await tool.execute({ track_id: '99' });
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
result: 'Track with ID "99" not found or is not a MIDI track.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors when the target is not a MIDI track', async () => {
|
||||||
|
const audioTrack = new KGAudioTrack('Vocal', 1);
|
||||||
|
const project = new KGProject('delete-audio-track-project');
|
||||||
|
project.setTracks([audioTrack]);
|
||||||
|
mockCore(project);
|
||||||
|
|
||||||
|
const tool = new DeleteTrackTool();
|
||||||
|
const result = await tool.execute({ track_id: '1' });
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
result: 'Track with ID "1" not found or is not a MIDI track.',
|
||||||
|
});
|
||||||
|
expect(tool.isReadOnlyTool()).toBe(false);
|
||||||
|
expect(tool.isAvailableInEfficientMode()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates confirmation content only for valid target input', () => {
|
||||||
|
const tool = new DeleteTrackTool();
|
||||||
|
|
||||||
|
expect(tool.buildConfirmationContent({ track_id: '1' })).toBe('Allow deleting track ID **1**?');
|
||||||
|
expect(tool.buildConfirmationContent({ track_name: 'Lead' })).toBe('Allow deleting track **Lead**?');
|
||||||
|
expect(tool.buildConfirmationContent({})).toBeUndefined();
|
||||||
|
expect(tool.buildConfirmationContent(null)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not generate display content for failed or malformed results', () => {
|
||||||
|
const tool = new DeleteTrackTool();
|
||||||
|
|
||||||
|
expect(tool.buildToolResultDisplayContent(
|
||||||
|
{ track_id: '1' },
|
||||||
|
{ success: false, result: 'Track with ID "1" not found or is not a MIDI track.' },
|
||||||
|
)).toBeUndefined();
|
||||||
|
|
||||||
|
expect(tool.buildToolResultDisplayContent(
|
||||||
|
{ track_id: '1' },
|
||||||
|
{ success: true, result: 'Track deleted.' },
|
||||||
|
)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { RemoveTrackCommand } from '../../core/commands/track/RemoveTrackCommand';
|
||||||
|
import { KGMidiTrack } from '../../core/track/KGMidiTrack';
|
||||||
|
import { BaseTool } from './BaseTool';
|
||||||
|
import type { ToolParameter, ToolResult } from './BaseTool';
|
||||||
|
import { resolveMidiTrackByExactName, resolveMidiTrackByIdOrName } from './toolTargeting';
|
||||||
|
|
||||||
|
export class DeleteTrackTool extends BaseTool {
|
||||||
|
readonly name = 'delete_track';
|
||||||
|
readonly description =
|
||||||
|
'Delete an existing MIDI track by track_id or track_name. Prefer track_id because track_name may be duplicated.';
|
||||||
|
|
||||||
|
readonly parameters: Record<string, ToolParameter> = {
|
||||||
|
track_id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Target MIDI track ID. Preferred when available.',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
track_name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Target MIDI track name. Used only when track_id is omitted.',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
override isReadOnlyTool(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
override isAvailableInEfficientMode(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
override buildConfirmationContent(args: Record<string, unknown> | null): string | undefined {
|
||||||
|
if (!args) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof args.track_id === 'string') {
|
||||||
|
return `Allow deleting track ID **${args.track_id}**?`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof args.track_name === 'string') {
|
||||||
|
return `Allow deleting track **${args.track_name}**?`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
override buildToolResultDisplayContent(args: Record<string, unknown> | null, toolResult: ToolResult): string | undefined {
|
||||||
|
if (!args || !toolResult.success) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackIdMatch = toolResult.result.match(/track_id:\s*(\d+)/);
|
||||||
|
const trackNameMatch = toolResult.result.match(/track_name:\s*(.+)/);
|
||||||
|
if (!trackIdMatch || !trackNameMatch) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'Track deleted:',
|
||||||
|
`- track_id: ${trackIdMatch[1]}`,
|
||||||
|
`- track_name: ${trackNameMatch[1]}`,
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(params: Record<string, unknown>): Promise<ToolResult> {
|
||||||
|
try {
|
||||||
|
this.validateParameters(params);
|
||||||
|
|
||||||
|
const trackId = params.track_id as string | undefined;
|
||||||
|
const trackName = params.track_name as string | undefined;
|
||||||
|
|
||||||
|
if (!trackId && !trackName) {
|
||||||
|
return this.createErrorResult('Either track_id or track_name must be provided.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!trackId && trackName) {
|
||||||
|
const matchingTracks = resolveMidiTrackByExactName(trackName);
|
||||||
|
if (matchingTracks.length > 1) {
|
||||||
|
return this.createErrorResult(`Multiple MIDI tracks share the name "${trackName}". Provide track_id instead.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedTrack = resolveMidiTrackByIdOrName(trackId, trackName);
|
||||||
|
if (!resolvedTrack) {
|
||||||
|
return this.createErrorResult(
|
||||||
|
trackId
|
||||||
|
? `Track with ID "${trackId}" not found or is not a MIDI track.`
|
||||||
|
: `Track with name "${trackName}" not found or is not a MIDI track.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(resolvedTrack instanceof KGMidiTrack)) {
|
||||||
|
return this.createErrorResult(
|
||||||
|
trackId
|
||||||
|
? `Track with ID "${trackId}" not found or is not a MIDI track.`
|
||||||
|
: `Track with name "${trackName}" not found or is not a MIDI track.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedTrackId = resolvedTrack.getId().toString();
|
||||||
|
const deletedTrackName = resolvedTrack.getName();
|
||||||
|
const command = new RemoveTrackCommand(resolvedTrack.getId());
|
||||||
|
await this.executeCommand(command);
|
||||||
|
|
||||||
|
return this.createSuccessResult([
|
||||||
|
'Track deleted:',
|
||||||
|
`track_id: ${deletedTrackId}`,
|
||||||
|
`track_name: ${deletedTrackName}`,
|
||||||
|
].join('\n'));
|
||||||
|
} catch (error) {
|
||||||
|
return this.createErrorResult(`Failed to delete track: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import { ListAllTracksTool } from './ListAllTracksTool';
|
|||||||
import { ListAllAvailableInstrumentsTool } from './ListAllAvailableInstrumentsTool';
|
import { ListAllAvailableInstrumentsTool } from './ListAllAvailableInstrumentsTool';
|
||||||
import { CreateNewTrackTool } from './CreateNewTrackTool';
|
import { CreateNewTrackTool } from './CreateNewTrackTool';
|
||||||
import { UpdateTrackTool } from './UpdateTrackTool';
|
import { UpdateTrackTool } from './UpdateTrackTool';
|
||||||
|
import { DeleteTrackTool } from './DeleteTrackTool';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AddNotesTool,
|
AddNotesTool,
|
||||||
@@ -26,6 +27,7 @@ export {
|
|||||||
ListAllAvailableInstrumentsTool,
|
ListAllAvailableInstrumentsTool,
|
||||||
CreateNewTrackTool,
|
CreateNewTrackTool,
|
||||||
UpdateTrackTool,
|
UpdateTrackTool,
|
||||||
|
DeleteTrackTool,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tool registry for easy access
|
// Tool registry for easy access
|
||||||
@@ -40,6 +42,7 @@ export const AVAILABLE_TOOLS = {
|
|||||||
list_all_available_instruments: ListAllAvailableInstrumentsTool,
|
list_all_available_instruments: ListAllAvailableInstrumentsTool,
|
||||||
create_new_track: CreateNewTrackTool,
|
create_new_track: CreateNewTrackTool,
|
||||||
update_track: UpdateTrackTool,
|
update_track: UpdateTrackTool,
|
||||||
|
delete_track: DeleteTrackTool,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type ToolName = keyof typeof AVAILABLE_TOOLS;
|
export type ToolName = keyof typeof AVAILABLE_TOOLS;
|
||||||
|
|||||||
Reference in New Issue
Block a user