/** * KGDebugger - Global debugging utility for KGSP * Provides console access to internal methods for testing and debugging */ import { KGCore } from './KGCore'; import { KGMidiRegion } from './region/KGMidiRegion'; import { convertRegionToABCNotation } from '../util/abcNotationUtil'; import { extractXMLFromString } from '../util/xmlUtil'; import { AgentCore } from '../agent/core/AgentCore'; import { AVAILABLE_TOOLS } from '../agent/tools'; import type { TimeSignature } from '../types/projectTypes'; import { useProjectStore } from '../stores/projectStore'; /** * Global debugger singleton for console-based testing */ export class KGDebugger { // Private static instance for singleton pattern private static _instance: KGDebugger | null = null; // Private constructor to prevent direct instantiation private constructor() { console.log("šŸ”§ KGDebugger initialized - Available methods:", [ 'convertSelectedRegionToABCNotation(startFromBeat?)', 'testQuantizeDuration(durationBeats, timeSignature?)', 'debugSelectedItems()', 'createTestRegion()', 'testExtractXMLFromString(input)', 'testToolCall(jsonInput)', 'inputChatBox(content, interval?)', 'opfs(command)', ]); } /** * Get the singleton instance of KGDebugger */ public static instance(): KGDebugger { if (!KGDebugger._instance) { KGDebugger._instance = new KGDebugger(); } return KGDebugger._instance; } /** * Convert the currently selected region to ABC notation * @param startFromBeat - Optional absolute beat position to start from (defaults to region start) */ public convertSelectedRegionToABCNotation(startFromBeat?: number): void { const core = KGCore.instance(); const selectedItems = core.getSelectedItems(); let midiRegion: KGMidiRegion | null = null; // First try to find MIDI region in selected items if (selectedItems && selectedItems.length > 0) { console.log(`šŸ“ Converting selected region to ABC notation...`); console.log(`šŸ“Š Selected items count: ${selectedItems.length}`); midiRegion = selectedItems.find(item => item.getCurrentType() === 'KGMidiRegion' ) as KGMidiRegion; } // If no MIDI region selected, try to use active region from piano roll if (!midiRegion) { console.log("šŸ“ No MIDI region selected, checking for active region..."); const storeState = useProjectStore.getState(); const activeRegionId = storeState.activeRegionId; const tracks = storeState.tracks; if (activeRegionId) { // Find the active region in tracks for (const track of tracks) { const regions = track.getRegions(); const region = regions.find(r => r.getId() === activeRegionId); if (region && region instanceof KGMidiRegion) { midiRegion = region; console.log(`āœ… Found active region: "${region.getName()}"`); break; } } } } if (!midiRegion) { console.error("āŒ No MIDI region found."); console.log("šŸ’” Try one of these:"); console.log(" • Select a region in the track grid"); console.log(" • Open a region in the piano roll editor"); return; } // Use provided startFromBeat or default to region start const effectiveStartBeat = startFromBeat ?? midiRegion.getStartFromBeat(); console.log(`šŸŽµ Converting region: "${midiRegion.getName()}"`); console.log(`šŸ“ Region starts at beat: ${midiRegion.getStartFromBeat()}`); console.log(`šŸ“ Conversion starts at beat: ${effectiveStartBeat}`); console.log(`šŸŽ¼ Notes in region: ${midiRegion.getNotes().length}`); try { const abcNotation = convertRegionToABCNotation(midiRegion, effectiveStartBeat); console.log("āœ… ABC Notation conversion successful!"); console.log("šŸ“„ Result:"); console.log("─".repeat(50)); console.log(abcNotation); console.log("─".repeat(50)); // Also copy to clipboard if possible if (navigator.clipboard) { navigator.clipboard.writeText(abcNotation).then(() => { console.log("šŸ“‹ ABC notation copied to clipboard!"); }).catch(() => { console.log("šŸ“‹ Could not copy to clipboard (requires HTTPS)"); }); } } catch (error) { console.error("āŒ Error converting to ABC notation:", error); } } /** * Test the quantization duration method with a specific duration * @param durationBeats - Duration in beats to test * @param timeSignature - Optional time signature (defaults to project time signature) */ public testQuantizeDuration(durationBeats: number, timeSignature?: TimeSignature): void { const core = KGCore.instance(); const project = core.getCurrentProject(); const effectiveTimeSignature = timeSignature ?? project.getTimeSignature(); console.log(`🧮 Testing quantization for ${durationBeats} beats...`); console.log(`ā±ļø Time signature: ${effectiveTimeSignature.numerator}/${effectiveTimeSignature.denominator}`); // Import quantization testing (we'll need to expose some internal methods) // For now, let's create a simple test const ticksPerBeat = 480 * (4 / effectiveTimeSignature.denominator); const durationTicks = Math.round(durationBeats * ticksPerBeat); console.log(`šŸŽµ Input: ${durationBeats} beats = ${durationTicks} ticks`); // Test different quantization values manually for demonstration const testValues = [ { name: '1/1', ticks: 1920 }, { name: '1/2', ticks: 960 }, { name: '1/3', ticks: 640 }, { name: '1/4', ticks: 480 }, { name: '1/6', ticks: 320 }, { name: '1/8', ticks: 240 }, { name: '1/12', ticks: 160 }, { name: '1/16', ticks: 120 } ]; console.log("šŸ“Š Quantization analysis:"); let bestMatch = { name: '1/4', error: Infinity, ticks: 480 }; testValues.forEach(val => { const remainder = durationTicks % val.ticks; const error = Math.min(remainder, val.ticks - remainder); const errorPercent = ((error / val.ticks) * 100).toFixed(1); if (error < bestMatch.error) { bestMatch = { name: val.name, error, ticks: val.ticks }; } console.log(` ${val.name}: ${error} ticks error (${errorPercent}%)`); }); const quantizedTicks = Math.round(durationTicks / bestMatch.ticks) * bestMatch.ticks; const quantizedBeats = quantizedTicks / ticksPerBeat; console.log(`āœ… Best match: ${bestMatch.name} grid`); console.log(`šŸŽÆ Quantized: ${quantizedBeats} beats = ${quantizedTicks} ticks`); console.log(`šŸ“ Difference: ${Math.abs(durationBeats - quantizedBeats).toFixed(4)} beats`); } /** * Debug currently selected items */ public debugSelectedItems(): void { const core = KGCore.instance(); const selectedItems = core.getSelectedItems(); console.log(`šŸ” Currently selected items: ${selectedItems.length}`); if (selectedItems.length === 0) { console.log("šŸ“ No items selected. Try selecting regions or notes first."); return; } selectedItems.forEach((item, index) => { const type = item.getCurrentType(); const id = item.getId(); console.log(` ${index + 1}. ${type} (ID: ${id})`); if (type === 'KGMidiRegion') { const region = item as KGMidiRegion; console.log(` šŸ“ Position: ${region.getStartFromBeat()} beats`); console.log(` šŸ“ Length: ${region.getLength()} beats`); console.log(` šŸŽµ Notes: ${region.getNotes().length}`); console.log(` šŸ“› Name: "${region.getName()}"`); } }); } /** * Create a test region with sample notes for testing (future implementation) */ public createTestRegion(): void { console.log("🚧 createTestRegion() - Not implemented yet"); console.log("šŸ’” This method would create a region with sample notes for testing"); console.log("šŸ’” For now, please create regions manually in the DAW interface"); } /** * Test the extractXMLFromString utility function with a given input * @param input - String that may contain XML blocks to extract */ public testExtractXMLFromString(input: string): void { console.log("šŸ” Testing extractXMLFromString utility..."); console.log("šŸ“ Input string:"); console.log("─".repeat(50)); console.log(input); console.log("─".repeat(50)); try { const xmlBlocks = extractXMLFromString(input); console.log(`āœ… Extraction successful! Found ${xmlBlocks.length} XML block(s):`); if (xmlBlocks.length === 0) { console.log("šŸ“­ No XML blocks found in the input string."); console.log("šŸ’” Try input with XML tags like: ..."); } else { xmlBlocks.forEach((block, index) => { console.log("\nšŸ“„ XML Block " + (index + 1) + ":"); console.log("─".repeat(30)); console.log(block); console.log("─".repeat(30)); }); // Copy all blocks to clipboard if possible if (navigator.clipboard && xmlBlocks.length > 0) { const allBlocks = xmlBlocks.join('\n\n'); navigator.clipboard.writeText(allBlocks).then(() => { console.log("šŸ“‹ XML blocks copied to clipboard!"); }).catch(() => { console.log("šŸ“‹ Could not copy to clipboard (requires HTTPS)"); }); } } } catch (error) { console.error("āŒ Error extracting XML:", error); } } /** * Test native tool calling by executing tool calls from a JSON string. * Accepts a single tool call object or an array of tool call objects. * * Usage examples in browser console: * * // Single tool call: * await KGStudio.KGDebugger.testToolCall('{"name":"read_music","arguments":{"start":0,"length":8}}') * * // Multiple tool calls: * await KGStudio.KGDebugger.testToolCall('[{"name":"remove_notes","arguments":{"start":0,"end_beat":4}},{"name":"add_notes","arguments":{"notes":[{"pitch":"C4","start":0,"length":1}]}}]') * * // Can also pass a JS object directly (no need to stringify): * await KGStudio.KGDebugger.testToolCall({name:"read_music",arguments:{start:0}}) * * @param input - JSON string, object, or array of tool call(s). * Each tool call should have: { name: string, arguments: object } */ public async testToolCall(input: string | Record | Record[]): Promise { try { // Parse input let calls: Array<{ name: string; arguments: Record }>; if (typeof input === 'string') { const parsed = JSON.parse(input); calls = Array.isArray(parsed) ? parsed : [parsed]; } else if (Array.isArray(input)) { calls = input as Array<{ name: string; arguments: Record }>; } else { calls = [input as { name: string; arguments: Record }]; } console.log(`šŸ”§ Executing ${calls.length} tool call(s)...\n`); for (let i = 0; i < calls.length; i++) { const call = calls[i]; const toolName = call.name; const toolArgs = call.arguments ?? {}; console.log(`── Tool call ${i + 1}/${calls.length}: ${toolName}`); console.log(` Arguments: ${JSON.stringify(toolArgs, null, 2)}`); const ToolClass = AVAILABLE_TOOLS[toolName as keyof typeof AVAILABLE_TOOLS]; if (!ToolClass) { console.error(` āŒ Unknown tool: "${toolName}". Available tools: ${Object.keys(AVAILABLE_TOOLS).join(', ')}`); continue; } const toolInstance = new ToolClass(); const result = await toolInstance.execute(toolArgs); // Sync UI state on success if (result.success) { useProjectStore.getState().refreshProjectState(); } const icon = result.success ? 'āœ…' : 'āŒ'; console.log(` ${icon} Success: ${result.success}`); console.log(` Result: ${result.result}\n`); } console.log('šŸ”§ Tool execution complete.'); } catch (error) { console.error('āŒ Error in testToolCall:', error); console.log('šŸ’” Expected format: {"name":"tool_name","arguments":{...}}'); console.log(' Or an array: [{"name":"tool1","arguments":{...}}, ...]'); } } /** * Show help information */ public help(): void { console.log("šŸ”§ KGDebugger Help"); console.log("Available methods:"); console.log(" convertSelectedRegionToABCNotation(startFromBeat?) - Convert selected region to ABC"); console.log(" testQuantizeDuration(beats, timeSignature?) - Test quantization logic"); console.log(" debugSelectedItems() - Show info about selected items"); console.log(" createTestRegion() - Create test region (not implemented)"); console.log(" testExtractXMLFromString(input) - Test XML extraction from string"); console.log(" testToolCall(input) - Execute tool call(s) from JSON and show results"); console.log(" inputChatBox(content, interval?) - Type into ChatBox textarea and submit with Enter"); console.log(" opfs(command) - OPFS file browser (pwd, ls, cd, cat, dl)"); console.log(" help() - Show this help"); console.log(""); console.log("šŸ’” Usage tips:"); console.log(" - Select regions in the DAW first, then run debug methods"); console.log(" - Results are logged to console and copied to clipboard when possible"); console.log(" - Use browser developer tools for best experience"); console.log(""); console.log("šŸ’” testToolCall examples:"); console.log(' await KGStudio.KGDebugger.testToolCall(\'{"name":"read_music","arguments":{"start":0,"length":8}}\')'); console.log(' await KGStudio.KGDebugger.testToolCall({name:"add_notes",arguments:{notes:[{pitch:"C4",start:0,length:1}]}})'); console.log(' await KGStudio.KGDebugger.testToolCall([{name:"remove_notes",arguments:{start:0,end_beat:4}},{name:"read_music",arguments:{}}])'); } /** * Type content into ChatBox textarea character-by-character and submit with Enter. * Honors auto-resize (by dispatching 'input' events) and ChatBox's Enter-to-send behavior. * @param content - The text to type into the chat input * @param interval - Delay in ms between characters (default 30ms) */ public async inputChatBox(content: string, interval: number = 30): Promise { try { const textarea = document.querySelector('textarea.chatbox-input') as HTMLTextAreaElement | null; if (!textarea) { console.error('āŒ ChatBox textarea not found. Ensure ChatBox is mounted and visible.'); return; } // Focus to trigger ChatBox focus handlers and ensure caret/attribute setup textarea.focus(); // Use native value setter to keep React's value tracker in sync const valueSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set; const setValue = (val: string) => { if (valueSetter) { valueSetter.call(textarea, val); } else { textarea.value = val; } }; // Helper to dispatch an InputEvent so React's onChange fires const dispatchInput = (data?: string) => { const ev = typeof InputEvent !== 'undefined' ? new InputEvent('input', { bubbles: true, data, inputType: 'insertText' }) : new Event('input', { bubbles: true }); textarea.dispatchEvent(ev); }; // Start from empty content to simulate a fresh user input setValue(''); dispatchInput(''); // Helper: sleep const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); let typed = ''; for (let i = 0; i < content.length; i++) { typed += content[i]; // Update the controlled textarea and dispatch input so React onChange fires setValue(typed); dispatchInput(content[i]); // Place caret at end for realism try { textarea.selectionStart = textarea.selectionEnd = typed.length; } catch { // noop } if (interval > 0) { await sleep(interval); } } // Give React a brief moment to commit the last setState and run auto-resize effect await sleep(Math.max(30, interval)); // Simulate pressing Enter to submit (ChatBox listens on keydown) const enterEvent = new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true } as KeyboardEventInit & { keyCode: number; which: number }); textarea.dispatchEvent(enterEvent); // Optional: follow-up keyup to mirror real typing (some UIs inspect it) const keyupEvent = new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, } as KeyboardEventInit & { keyCode: number; which: number }); textarea.dispatchEvent(keyupEvent); } catch (error) { console.error('āŒ Error in inputChatBox:', error); } } // --- OPFS Shell --- /** Current working directory path segments (relative to OPFS root) */ private opfsCwd: string[] = []; /** * Simplified bash-like shell for browsing the OPFS filesystem. * * Supported commands: * pwd — print current directory * ls — list files/folders (like ls -lla) * cd — change directory (supports .., /, relative, and quoted paths) * cat — print file contents * dl — download a file to your local machine * * Usage in console: * await KGDebugger.opfs('pwd') * await KGDebugger.opfs('ls') * await KGDebugger.opfs('cd projects') * await KGDebugger.opfs('cat project.json') */ public async opfs(command: string): Promise { const parts = command.trim().match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [] const cmd = parts[0] // Strip surrounding quotes from arguments const arg = parts.slice(1).map(p => p.replace(/^["']|["']$/g, '')).join(' ') try { switch (cmd) { case 'pwd': console.log('/' + this.opfsCwd.join('/')) break case 'ls': await this.opfsLs() break case 'cd': await this.opfsCd(arg) break case 'cat': await this.opfsCat(arg) break case 'dl': await this.opfsDl(arg) break default: console.log(`opfs: command not found: ${cmd}`) console.log('Available commands: pwd, ls, cd , cat , dl ') } } catch (error) { console.error(`opfs: ${error}`) } } private async opfsResolveCwd(): Promise { let dir = await navigator.storage.getDirectory() for (const segment of this.opfsCwd) { dir = await dir.getDirectoryHandle(segment) } return dir } private async opfsLs(): Promise { const dir = await this.opfsResolveCwd() const entries: Array<{ kind: string; name: string; size: number; modified: string }> = [] for await (const entry of dir.values()) { if (entry.kind === 'file') { const fileHandle = entry as FileSystemFileHandle const file = await fileHandle.getFile() entries.push({ kind: 'file', name: entry.name, size: file.size, modified: new Date(file.lastModified).toISOString().replace('T', ' ').slice(0, 19), }) } else { entries.push({ kind: 'dir', name: entry.name + '/', size: 0, modified: '-', }) } } // Sort: directories first, then files, alphabetically within each group entries.sort((a, b) => { if (a.kind !== b.kind) return a.kind === 'dir' ? -1 : 1 return a.name.localeCompare(b.name) }) if (entries.length === 0) { console.log('(empty directory)') return } // Print header const cwdPath = '/' + this.opfsCwd.join('/') console.log(`total ${entries.length} (${cwdPath})`) // Format like ls -lla const maxSizeLen = Math.max(...entries.map(e => String(e.size).length), 4) for (const e of entries) { const typeChar = e.kind === 'dir' ? 'd' : '-' const perms = e.kind === 'dir' ? 'rwxr-xr-x' : 'rw-r--r--' const sizeStr = e.kind === 'dir' ? '-'.padStart(maxSizeLen) : String(e.size).padStart(maxSizeLen) console.log(`${typeChar}${perms} ${sizeStr} ${e.modified} ${e.name}`) } } private async opfsCd(path: string): Promise { if (!path || path === '') { // cd with no args goes to root this.opfsCwd = [] return } let segments: string[] if (path === '/') { this.opfsCwd = [] return } else if (path.startsWith('/')) { // Absolute path segments = path.split('/').filter(Boolean) } else { // Relative path segments = [...this.opfsCwd, ...path.split('/').filter(Boolean)] } // Resolve . and .. const resolved: string[] = [] for (const seg of segments) { if (seg === '.') continue if (seg === '..') { resolved.pop() } else { resolved.push(seg) } } // Verify the path exists let dir = await navigator.storage.getDirectory() for (const seg of resolved) { try { dir = await dir.getDirectoryHandle(seg) } catch { console.error(`opfs: cd: no such directory: ${path}`) return } } this.opfsCwd = resolved } private async opfsCat(fileName: string): Promise { if (!fileName) { console.error('opfs: cat: missing file name') return } const dir = await this.opfsResolveCwd() try { const fileHandle = await dir.getFileHandle(fileName) const file = await fileHandle.getFile() const text = await file.text() // Pretty-print JSON files if (fileName.endsWith('.json')) { try { const parsed = JSON.parse(text) console.log(JSON.stringify(parsed, null, 2)) } catch { console.log(text) } } else { console.log(text) } } catch { console.error(`opfs: cat: ${fileName}: No such file`) } } private async opfsDl(fileName: string): Promise { if (!fileName) { console.error('opfs: dl: missing file name') return } const dir = await this.opfsResolveCwd() try { const fileHandle = await dir.getFileHandle(fileName) const file = await fileHandle.getFile() const url = URL.createObjectURL(file) const a = document.createElement('a') a.href = url a.download = fileName document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) console.log(`downloaded: ${fileName} (${file.size} bytes)`) } catch { console.error(`opfs: dl: ${fileName}: No such file`) } } }