initial public release.

This commit is contained in:
Xiaohan-Tian
2025-08-11 18:37:21 -07:00
commit de51967b49
186 changed files with 32322 additions and 0 deletions
+235
View File
@@ -0,0 +1,235 @@
import { BaseTool } from './BaseTool';
import type { ToolResult, ToolParameter } from './BaseTool';
import { CreateNotesCommand } from '../../core/commands/note/CreateNotesCommand';
import type { NoteCreationData } from '../../core/commands/note/CreateNotesCommand';
import { KGMidiRegion } from '../../core/region/KGMidiRegion';
import { useProjectStore } from '../../stores/projectStore';
import { KGCore } from '../../core/KGCore';
/**
* Tool for adding notes to MIDI regions
* Integrates with the existing command system for undo/redo support
*/
export class AddNotesTool extends BaseTool {
readonly name = 'add_notes';
readonly description = 'Create one or more MIDI notes in the current region. Each note requires pitch (e.g., "C4", "F#3"), start_beat (beat position), and length (duration in beats).';
readonly parameters: Record<string, ToolParameter> = {
notes: {
type: 'array',
description: 'Array of notes to create',
required: true,
items: {
type: 'object',
description: 'A MIDI note definition',
properties: {
pitch: {
type: 'string',
description: 'Note pitch in scientific notation (e.g., "C4", "F#3", "Bb2")',
required: true
},
start_beat: {
type: 'number',
description: 'Start position in beats (e.g., 0, 1.5, 2)',
required: true
},
length: {
type: 'number',
description: 'Note duration in beats (e.g., 1, 0.5, 4)',
required: true
},
velocity: {
type: 'number',
description: 'Note velocity (1-127, default: 127)',
required: false
}
}
}
},
region_id: {
type: 'string',
description: 'ID of the region to add notes to. If not provided, uses the currently selected region.',
required: false
}
};
async execute(params: Record<string, unknown>): Promise<ToolResult> {
try {
// Validate parameters
this.validateParameters(params);
const notes = params.notes as Array<{
pitch: string;
start_beat: number;
length: number;
velocity?: number;
}>;
const regionId = params.region_id as string | undefined;
// Find the target region
const targetRegion = this.findTargetRegion(regionId);
if (!targetRegion) {
return this.createErrorResult(
regionId
? `Region with ID "${regionId}" not found or is not a MIDI region`
: 'No active or selected MIDI region found. Please open the piano roll with a region or select a MIDI region first.'
);
}
// Validate and convert notes to creation data
const noteCreationData: NoteCreationData[] = [];
const createdNotes: Array<{ pitch: string; start_beat: number; length: number }> = [];
for (const note of notes) {
try {
const midiPitch = this.convertPitchToMidi(note.pitch);
const velocity = note.velocity ?? 127;
// Validate velocity range
if (velocity < 1 || velocity > 127) {
return this.createErrorResult(`Invalid velocity ${velocity}. Must be between 1 and 127.`);
}
// Validate beat positions
if (note.start_beat < 0) {
return this.createErrorResult(`Invalid start_beat ${note.start_beat}. Must be >= 0.`);
}
if (note.length <= 0) {
return this.createErrorResult(`Invalid length ${note.length}. Must be > 0.`);
}
// Adjust note position relative to region's start beat
const regionStartBeat = targetRegion.getStartFromBeat();
const adjustedStartBeat = note.start_beat - regionStartBeat;
const adjustedEndBeat = adjustedStartBeat + note.length;
// Create note creation data
noteCreationData.push({
regionId: targetRegion.getId(),
startBeat: adjustedStartBeat,
endBeat: adjustedEndBeat,
pitch: midiPitch,
velocity
});
createdNotes.push({
pitch: note.pitch,
start_beat: note.start_beat,
length: note.length
});
} catch (error) {
return this.createErrorResult(`Invalid note pitch "${note.pitch}": ${error}`);
}
}
// Execute the bulk note creation command
const command = new CreateNotesCommand(noteCreationData);
await this.executeCommand(command);
// Create success message
const noteCount = createdNotes.length;
const noteList = createdNotes
.map(note => `${note.pitch} (beat ${note.start_beat}, length ${note.length})`)
.join(', ');
return this.createSuccessResult(
`Successfully created ${noteCount} note${noteCount > 1 ? 's' : ''}: ${noteList}`
);
} catch (error) {
return this.createErrorResult(`Failed to create notes: ${error}`);
}
}
/**
* Find the target region for note creation
* Priority: 1) Specified regionId, 2) Active piano roll region, 3) Selected regions, 4) Error if none found
*/
private findTargetRegion(regionId?: string): KGMidiRegion | null {
const project = this.getCurrentProject();
const tracks = project.getTracks();
if (regionId) {
// Find specific region by ID
for (const track of tracks) {
const regions = track.getRegions();
const region = regions.find(r => r.getId() === regionId);
if (region && region instanceof KGMidiRegion) {
return region;
}
}
return null;
} else {
// Smart region finding: try different sources in priority order
// 1. Try active piano roll region
const storeState = useProjectStore.getState();
if (storeState.activeRegionId) {
for (const track of tracks) {
const regions = track.getRegions();
const region = regions.find(r => r.getId() === storeState.activeRegionId);
if (region && region instanceof KGMidiRegion) {
return region;
}
}
}
// 2. Try selected regions
const core = this.getKGCore();
const selectedItems = core.getSelectedItems();
for (const item of selectedItems) {
if (item instanceof KGMidiRegion) {
return item;
}
}
// 3. No fallback - return null to trigger error
return null;
}
}
/**
* Get KGCore instance for selection access
*/
private getKGCore() {
return KGCore.instance();
}
/**
* Convert pitch string to MIDI note number
* Supports formats like: C4, F#3, Bb2, C#5
*/
private convertPitchToMidi(pitch: string): number {
const match = pitch.match(/^([A-G])([#b]?)(\d+)$/);
if (!match) {
throw new Error(`Invalid pitch format "${pitch}". Use format like "C4", "F#3", "Bb2"`);
}
const [, noteName, accidental, octaveStr] = match;
const octave = parseInt(octaveStr);
// Base MIDI notes for C octave (C4 = 60)
const noteOffsets: Record<string, number> = {
'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11
};
let midiNote = (octave + 1) * 12 + noteOffsets[noteName];
// Apply accidentals
if (accidental === '#') {
midiNote += 1;
} else if (accidental === 'b') {
midiNote -= 1;
}
// Validate MIDI range
if (midiNote < 0 || midiNote > 127) {
throw new Error(`Note "${pitch}" is out of MIDI range (0-127)`);
}
return midiNote;
}
}
+49
View File
@@ -0,0 +1,49 @@
import { BaseTool } from './BaseTool';
import type { ToolResult, ToolParameter } from './BaseTool';
import { AgentCore } from '../core/AgentCore';
/**
* Tool for signaling task completion
* This is a pure agent state tool that doesn't modify the DAW but signals
* to the agent system that the user's requested task has been completed
*/
export class AttemptCompletionTool extends BaseTool {
readonly name = 'attempt_completion';
readonly description = 'Signal that the current user task is fully complete. Only use this when you have successfully fulfilled all aspects of the user\'s request.';
readonly parameters: Record<string, ToolParameter> = {
comment: {
type: 'string',
description: 'A brief comment describing what was completed and any relevant details about the task fulfillment.',
required: true
}
};
async execute(params: Record<string, unknown>): Promise<ToolResult> {
try {
// Validate parameters
this.validateParameters(params);
const comment = params.comment as string;
// Validate comment is not empty
if (!comment.trim()) {
return this.createErrorResult('Comment cannot be empty. Please provide a meaningful completion summary.');
}
// Get current agent state and update task completion status
const agentCore = AgentCore.instance();
const agentState = agentCore.getAgentState();
// Mark that we're no longer working on a task
agentState.setIsWorkingOnTask(false);
return this.createSuccessResult(
`Task completed: ${comment}. Agent task status updated to not working.`
);
} catch (error) {
return this.createErrorResult(`Failed to mark task as complete: ${error}`);
}
}
}
+162
View File
@@ -0,0 +1,162 @@
import { KGCommand } from '../../core/commands/KGCommand';
import { KGCore } from '../../core/KGCore';
/**
* Result of tool execution
*/
export interface ToolResult {
success: boolean;
result: string;
}
/**
* Parameter definition for tool parameters
*/
export interface ToolParameter {
type: 'string' | 'number' | 'boolean' | 'array' | 'object';
description: string;
required?: boolean;
items?: ToolParameter; // For array types
properties?: Record<string, ToolParameter>; // For object types
}
/**
* Tool definition schema
*/
export interface ToolDefinition {
name: string;
description: string;
parameters: Record<string, ToolParameter>;
}
/**
* Abstract base class for all agent tools
* Provides integration with the existing command system and core architecture
*/
export abstract class BaseTool {
abstract readonly name: string;
abstract readonly description: string;
abstract readonly parameters: Record<string, ToolParameter>;
/**
* Execute the tool with given parameters
* @param params Tool parameters
* @returns Promise resolving to tool execution result
*/
abstract execute(params: Record<string, unknown>): Promise<ToolResult>;
/**
* Get the tool definition in OpenAI function calling format
*/
getDefinition(): ToolDefinition {
return {
name: this.name,
description: this.description,
parameters: this.parameters
};
}
/**
* Validate parameters against the tool's parameter schema
* @param params Parameters to validate
* @returns True if valid, throws error if invalid
*/
protected validateParameters(params: Record<string, unknown>): boolean {
for (const [paramName, paramDef] of Object.entries(this.parameters)) {
const value = params[paramName];
// Check required parameters
if (paramDef.required && (value === undefined || value === null)) {
throw new Error(`Required parameter '${paramName}' is missing`);
}
// Skip type checking for undefined optional parameters
if (value === undefined || value === null) {
continue;
}
// Type validation
if (!this.validateParameterType(value, paramDef)) {
throw new Error(`Parameter '${paramName}' has invalid type. Expected: ${paramDef.type}`);
}
}
return true;
}
/**
* Validate a single parameter value against its type definition
*/
private validateParameterType(value: unknown, paramDef: ToolParameter): boolean {
switch (paramDef.type) {
case 'string':
return typeof value === 'string';
case 'number':
return typeof value === 'number';
case 'boolean':
return typeof value === 'boolean';
case 'array':
if (!Array.isArray(value)) return false;
if (paramDef.items) {
return value.every(item => this.validateParameterType(item, paramDef.items!));
}
return true;
case 'object':
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
return false;
}
if (paramDef.properties) {
const obj = value as Record<string, unknown>;
for (const [propName, propDef] of Object.entries(paramDef.properties)) {
if (propDef.required && !(propName in obj)) {
return false;
}
if (propName in obj && !this.validateParameterType(obj[propName], propDef)) {
return false;
}
}
}
return true;
default:
return false;
}
}
/**
* Execute a command through the existing command system
* This provides undo/redo functionality and proper state management
* @param command Command to execute
*/
protected async executeCommand(command: KGCommand): Promise<void> {
const core = KGCore.instance();
return core.executeCommand(command);
}
/**
* Get the current project from KGCore
*/
protected getCurrentProject() {
const core = KGCore.instance();
return core.getCurrentProject();
}
/**
* Create a successful tool result
*/
protected createSuccessResult(result: string): ToolResult {
return {
success: true,
result
};
}
/**
* Create a failed tool result
*/
protected createErrorResult(result: string): ToolResult {
return {
success: false,
result
};
}
}
+299
View File
@@ -0,0 +1,299 @@
import { BaseTool } from './BaseTool';
import type { ToolResult, ToolParameter } from './BaseTool';
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
* Provides read-only access to project data and converts to ABC notation
*/
export class ReadMusicTool extends BaseTool {
readonly name = 'read_music';
readonly description = 'Read the music content from a specific track or all tracks, returning the content in ABC notation format.';
readonly parameters: Record<string, ToolParameter> = {
track_id: {
type: 'string',
description: 'The track ID to read, or "all" to read all tracks. If not provided, reads the first available track.',
required: false
},
start_beat: {
type: 'number',
description: 'Start beat position to read from (default: 0)',
required: false
},
length: {
type: 'number',
description: 'Length in beats to read (default: entire track/project)',
required: false
}
};
async execute(params: Record<string, unknown>): Promise<ToolResult> {
try {
// Validate parameters
this.validateParameters(params);
const trackId = params.track_id as string | undefined;
const startBeat = (params.start_beat as number) || 0;
const length = params.length as number | undefined;
const project = this.getCurrentProject();
const tracks = project.getTracks();
if (tracks.length === 0) {
return this.createErrorResult('No tracks found in the project');
}
// Validate start_beat
if (startBeat < 0) {
return this.createErrorResult(`Invalid start_beat ${startBeat}. Must be >= 0.`);
}
// Validate length
if (length !== undefined && length <= 0) {
return this.createErrorResult(`Invalid length ${length}. Must be > 0.`);
}
// Get project settings for bar rounding
const timeSignature = project.getTimeSignature();
const beatsPerBar = timeSignature.numerator;
// Round startBeat to floor bar beats and calculate endBeat
const roundedStartBeat = Math.floor(startBeat / beatsPerBar) * beatsPerBar;
const rawEndBeat = length !== undefined ? startBeat + length : undefined;
const roundedEndBeat = rawEndBeat !== undefined ? Math.ceil(rawEndBeat / beatsPerBar) * beatsPerBar : undefined;
let abcOutput = '';
if (!trackId || trackId === '' || trackId === 'all') {
// Read all tracks
const midiTracks = tracks.filter(track => track instanceof KGMidiTrack) as KGMidiTrack[];
abcOutput = this.generateAllTracksABC(midiTracks, roundedStartBeat, roundedEndBeat);
} else {
// Read specific track or first available track
const targetTrack = trackId
? tracks.find(t => t.getId().toString() === trackId)
: tracks[0];
if (!targetTrack) {
return this.createErrorResult(
trackId
? `Track with ID "${trackId}" not found`
: 'No tracks available'
);
}
if (!(targetTrack instanceof KGMidiTrack)) {
return this.createErrorResult(`Track "${targetTrack.getName()}" is not a MIDI track`);
}
abcOutput = this.generateSingleTrackABC(targetTrack, roundedStartBeat, roundedEndBeat);
}
return this.createSuccessResult(abcOutput);
} catch (error) {
return this.createErrorResult(`Failed to read music: ${error}`);
}
}
/**
* Get KGCore instance
*/
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 {
try {
const store = useProjectStore.getState();
const core = this.getKGCore();
// 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;
}
}
// 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;
} catch (error) {
console.error('Error finding track to skip:', error);
return null;
}
}
/**
* 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
*/
private generateAllTracksABC(tracks: KGMidiTrack[], startBeat: number, endBeat?: number): string {
const midiTracks = tracks.filter(track => track instanceof KGMidiTrack);
if (midiTracks.length === 0) {
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
// Get project settings for proper notation
const project = this.getCurrentProject();
const timeSignature = project.getTimeSignature();
const keySignature = project.getKeySignature();
const abcKeySignature = KEY_SIGNATURE_MAP[keySignature]?.abcNotationKeySignature || 'C';
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) {
return; // Skip this track
}
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`;
// Get all regions from the track and convert each one
const regions = track.getRegions().filter(region => region instanceof KGMidiRegion) as KGMidiRegion[];
if (regions.length === 0) {
output += 'X:' + trackNumber + '\n';
output += `M:${timeSignature.numerator}/${timeSignature.denominator}\n`;
output += `K:${abcKeySignature}\n`;
output += 'z4 | // No regions found\n\n';
} else {
// Convert each region that overlaps with the requested range
let hasContent = false;
regions.forEach((region) => {
const regionStart = region.getStartFromBeat();
const regionEnd = regionStart + region.getLength();
// Check if region overlaps with requested range
if (regionStart < (endBeat || Infinity) && regionEnd > startBeat) {
const abcNotation = convertRegionToABCNotation(region, startBeat, endBeat);
// Update the X: line to include track number
const lines = abcNotation.split('\n');
lines[0] = `X:${trackNumber}`;
output += lines.join('\n') + '\n\n';
hasContent = true;
}
});
if (!hasContent) {
output += 'X:' + trackNumber + '\n';
output += `M:${timeSignature.numerator}/${timeSignature.denominator}\n`;
output += `K:${abcKeySignature}\n`;
output += 'z4 | // No content in specified range\n\n';
}
}
});
return output.trim();
}
/**
* Generate ABC notation for a single track
*/
private generateSingleTrackABC(track: KGMidiTrack, startBeat: number, endBeat?: number): string {
if (!(track instanceof KGMidiTrack)) {
return `Track is not a MIDI track.`;
}
// Get project settings for proper notation
const project = this.getCurrentProject();
const timeSignature = project.getTimeSignature();
const keySignature = project.getKeySignature();
const abcKeySignature = KEY_SIGNATURE_MAP[keySignature]?.abcNotationKeySignature || 'C';
const trackName = track.getName() || 'Unnamed Track';
let output = `Track "${trackName}" (beats ${startBeat}-${endBeat || 'end'}):\n`;
// Get all regions from the track and convert each one
const regions = track.getRegions().filter(region => region instanceof KGMidiRegion) as KGMidiRegion[];
if (regions.length === 0) {
output += 'X:1\n';
output += `T:${trackName}\n`;
output += `M:${timeSignature.numerator}/${timeSignature.denominator}\n`;
output += `K:${abcKeySignature}\n`;
output += `L:1/${timeSignature.denominator}\n`;
output += 'z4 | // No regions found';
} else {
// Convert each region that overlaps with the requested range
let hasContent = false;
regions.forEach((region) => {
const regionStart = region.getStartFromBeat();
const regionEnd = regionStart + region.getLength();
// Check if region overlaps with requested range
if (regionStart < (endBeat || Infinity) && regionEnd > startBeat) {
const abcNotation = convertRegionToABCNotation(region, startBeat, endBeat);
// Update the title to include track name
const lines = abcNotation.split('\n');
lines[1] = `T:${trackName}`;
output += lines.join('\n');
hasContent = true;
}
});
if (!hasContent) {
output += 'X:1\n';
output += `T:${trackName}\n`;
output += `M:${timeSignature.numerator}/${timeSignature.denominator}\n`;
output += `K:${abcKeySignature}\n`;
output += `L:1/${timeSignature.denominator}\n`;
output += 'z4 | // No content in specified range';
}
}
return output;
}
}
+168
View File
@@ -0,0 +1,168 @@
import { BaseTool } from './BaseTool';
import type { ToolResult, ToolParameter } from './BaseTool';
import { DeleteNotesCommand } from '../../core/commands/note/DeleteNotesCommand';
import { KGMidiRegion } from '../../core/region/KGMidiRegion';
import { useProjectStore } from '../../stores/projectStore';
import { KGCore } from '../../core/KGCore';
/**
* Tool for removing notes from MIDI regions within a specified beat range
* Integrates with the existing command system for undo/redo support
*/
export class RemoveNotesTool extends BaseTool {
readonly name = 'remove_notes';
readonly description = 'Remove MIDI notes from the current region within a specified beat range. All notes that start within the range will be deleted.';
readonly parameters: Record<string, ToolParameter> = {
start_beat: {
type: 'number',
description: 'Start of the beat range to remove notes from (inclusive)',
required: true
},
end_beat: {
type: 'number',
description: 'End of the beat range to remove notes from (exclusive)',
required: true
},
region_id: {
type: 'string',
description: 'ID of the region to remove notes from. If not provided, uses the currently selected region.',
required: false
}
};
async execute(params: Record<string, unknown>): Promise<ToolResult> {
try {
// Validate parameters
this.validateParameters(params);
const startBeat = params.start_beat as number;
const endBeat = params.end_beat as number;
const regionId = params.region_id as string | undefined;
// Validate beat range
if (startBeat < 0) {
return this.createErrorResult(`Invalid start_beat ${startBeat}. Must be >= 0.`);
}
if (endBeat <= startBeat) {
return this.createErrorResult(`Invalid beat range: end_beat (${endBeat}) must be greater than start_beat (${startBeat}).`);
}
// Find the target region
const targetRegion = this.findTargetRegion(regionId);
if (!targetRegion) {
return this.createErrorResult(
regionId
? `Region with ID "${regionId}" not found or is not a MIDI region`
: 'No active or selected MIDI region found. Please open the piano roll with a region or select a MIDI region first.'
);
}
// Adjust beat range relative to region's start beat
const regionStartBeat = targetRegion.getStartFromBeat();
const adjustedStartBeat = startBeat - regionStartBeat;
const adjustedEndBeat = endBeat - regionStartBeat;
// Find all notes within the specified beat range
const notesToRemove = this.findNotesInRange(targetRegion, adjustedStartBeat, adjustedEndBeat);
if (notesToRemove.length === 0) {
return this.createSuccessResult(
`No notes found in the range from beat ${startBeat} to ${endBeat}.`
);
}
// Extract note IDs for deletion
const noteIds = notesToRemove.map(note => note.getId());
// Execute the deletion command
const command = new DeleteNotesCommand(noteIds);
await this.executeCommand(command);
// Create success message
const noteCount = notesToRemove.length;
const noteList = notesToRemove
.map(note => {
const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
const octave = Math.floor(note.getPitch() / 12) - 1;
const noteName = noteNames[note.getPitch() % 12];
return `${noteName}${octave}`;
})
.join(', ');
return this.createSuccessResult(
`Successfully removed ${noteCount} note${noteCount > 1 ? 's' : ''} from beats ${startBeat}-${endBeat}: ${noteList}`
);
} catch (error) {
return this.createErrorResult(`Failed to remove notes: ${error}`);
}
}
/**
* Find the target region for note removal
* Priority: 1) Specified regionId, 2) Active piano roll region, 3) Selected regions, 4) Error if none found
*/
private findTargetRegion(regionId?: string): KGMidiRegion | null {
const project = this.getCurrentProject();
const tracks = project.getTracks();
if (regionId) {
// Find specific region by ID
for (const track of tracks) {
const regions = track.getRegions();
const region = regions.find(r => r.getId() === regionId);
if (region && region instanceof KGMidiRegion) {
return region;
}
}
return null;
} else {
// Smart region finding: try different sources in priority order
// 1. Try active piano roll region
const storeState = useProjectStore.getState();
if (storeState.activeRegionId) {
for (const track of tracks) {
const regions = track.getRegions();
const region = regions.find(r => r.getId() === storeState.activeRegionId);
if (region && region instanceof KGMidiRegion) {
return region;
}
}
}
// 2. Try selected regions
const core = this.getKGCore();
const selectedItems = core.getSelectedItems();
for (const item of selectedItems) {
if (item instanceof KGMidiRegion) {
return item;
}
}
// 3. No fallback - return null to trigger error
return null;
}
}
/**
* Get KGCore instance for selection access
*/
private getKGCore() {
return KGCore.instance();
}
/**
* Find all notes within the specified beat range
* Notes are included if their start beat is within [startBeat, endBeat)
*/
private findNotesInRange(region: KGMidiRegion, startBeat: number, endBeat: number) {
const notes = region.getNotes();
return notes.filter(note => {
const noteStartBeat = note.getStartBeat();
return noteStartBeat >= startBeat && noteStartBeat < endBeat;
});
}
}
+35
View File
@@ -0,0 +1,35 @@
import { BaseTool } from './BaseTool';
import type { ToolResult, ToolParameter } from './BaseTool';
/**
* Pseudo tool for handling <think> tags in LLM responses
* This tool displays the thinking content in the UI but doesn't send results back to the LLM
* Handles XML format: <think>any content here</think>
* This is functionally identical to ThinkingTool but handles the shorter tag name
*/
export class ThinkTool extends BaseTool {
readonly name = 'think';
readonly description = 'Pseudo tool for handling LLM thinking content from <think> tags. Shows content in UI but does not send results back to LLM.';
readonly parameters: Record<string, ToolParameter> = {
content: {
type: 'string',
description: 'The thinking content from the XML tag',
required: false
}
};
async execute(params: Record<string, unknown>): Promise<ToolResult> {
try {
// Extract the thinking content from the parameters
// const content = params.content as string || '';
// Return the thinking content as a successful result
// This will be displayed in the UI but not sent back to the LLM
return this.createSuccessResult("Thinking completed.");
} catch (error) {
return this.createErrorResult(`Failed to process thinking content: ${error}`);
}
}
}
+34
View File
@@ -0,0 +1,34 @@
import { BaseTool } from './BaseTool';
import type { ToolResult, ToolParameter } from './BaseTool';
/**
* Pseudo tool for handling <thinking> tags in LLM responses
* This tool displays the thinking content in the UI but doesn't send results back to the LLM
* Handles XML format: <thinking>any content here</thinking>
*/
export class ThinkingTool extends BaseTool {
readonly name = 'thinking';
readonly description = 'Pseudo tool for handling LLM thinking content. Shows content in UI but does not send results back to LLM.';
readonly parameters: Record<string, ToolParameter> = {
content: {
type: 'string',
description: 'The thinking content from the XML tag',
required: false
}
};
async execute(params: Record<string, unknown>): Promise<ToolResult> {
try {
// Extract the thinking content from the parameters
// const content = params.content as string || '';
// Return the thinking content as a successful result
// This will be displayed in the UI but not sent back to the LLM
return this.createSuccessResult("Thinking completed.");
} catch (error) {
return this.createErrorResult(`Failed to process thinking content: ${error}`);
}
}
}
+25
View File
@@ -0,0 +1,25 @@
// Base tool system
export { BaseTool } from './BaseTool';
export type { ToolResult, ToolParameter, ToolDefinition } from './BaseTool';
// Specific tools
import { AddNotesTool } from './AddNotesTool';
import { RemoveNotesTool } from './RemoveNotesTool';
import { ReadMusicTool } from './ReadMusicTool';
import { AttemptCompletionTool } from './AttemptCompletionTool';
import { ThinkingTool } from './ThinkingTool';
import { ThinkTool } from './ThinkTool';
export { AddNotesTool, RemoveNotesTool, ReadMusicTool, AttemptCompletionTool, ThinkingTool, ThinkTool };
// Tool registry for easy access
export const AVAILABLE_TOOLS = {
add_notes: AddNotesTool,
remove_notes: RemoveNotesTool,
read_music: ReadMusicTool,
attempt_completion: AttemptCompletionTool,
thinking: ThinkingTool,
think: ThinkTool
} as const;
export type ToolName = keyof typeof AVAILABLE_TOOLS;