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 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 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.
|
||||
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.
|
||||
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.
|
||||
- **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`.
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
@@ -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 { CreateNewTrackTool } from './CreateNewTrackTool';
|
||||
import { UpdateTrackTool } from './UpdateTrackTool';
|
||||
import { DeleteTrackTool } from './DeleteTrackTool';
|
||||
|
||||
export {
|
||||
AddNotesTool,
|
||||
@@ -26,6 +27,7 @@ export {
|
||||
ListAllAvailableInstrumentsTool,
|
||||
CreateNewTrackTool,
|
||||
UpdateTrackTool,
|
||||
DeleteTrackTool,
|
||||
};
|
||||
|
||||
// Tool registry for easy access
|
||||
@@ -40,6 +42,7 @@ export const AVAILABLE_TOOLS = {
|
||||
list_all_available_instruments: ListAllAvailableInstrumentsTool,
|
||||
create_new_track: CreateNewTrackTool,
|
||||
update_track: UpdateTrackTool,
|
||||
delete_track: DeleteTrackTool,
|
||||
} as const;
|
||||
|
||||
export type ToolName = keyof typeof AVAILABLE_TOOLS;
|
||||
|
||||
Reference in New Issue
Block a user