// ==DEBUG PANEL START== // Module: debug-panel.js // Depends on: config.js, logger.js, queue.js, storage.js // Purpose: In-page draggable panel showing recent logs and exposing tools/settings. // - Logs tab: tail of the Logger buffer with copy buttons // - Tools & Settings: toggles and numeric inputs bound to config, quick actions // - Pause/Stop controls and queue size indicator // The panel stores its position/collapsed state in localStorage (see config.js STORAGE_KEYS). /* global GM_notification */ (function () { const cfg = () => window.AI_REPO_CONFIG; const log = () => window.AI_REPO_LOGGER; const STORAGE_KEYS = window.AI_REPO_STORAGE_KEYS; class DebugPanel { constructor() { this.panel = null; this.bodyLogs = null; this.bodyTools = null; this.collapsed = false; this.drag = { active: false, dx: 0, dy: 0 }; this.panelState = this._loadPanelState(); } _loadPanelState() { try { return JSON.parse(localStorage.getItem(STORAGE_KEYS.panel) || '{}'); } catch { return {}; } } _savePanelState(partial) { try { const merged = { ...(this.panelState || {}), ...(partial || {}) }; this.panelState = merged; localStorage.setItem(STORAGE_KEYS.panel, JSON.stringify(merged)); } catch {} } flashBtn(btn, label = 'Done', ms = 900) { if (!btn) return; const old = btn.textContent; btn.disabled = true; btn.textContent = `${label} ✓`; btn.style.opacity = '0.7'; setTimeout(() => { btn.disabled = false; btn.textContent = old; btn.style.opacity = ''; }, ms); } toast(msg, ms = 1200) { if (!this.panel) return; const t = document.createElement('div'); t.textContent = msg; t.style.cssText = ` position:absolute; right:12px; bottom:12px; padding:6px 10px; background:#111827; color:#e5e7eb; border:1px solid #374151; border-radius:6px; font:12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; opacity:.98; pointer-events:none; box-shadow:0 6px 20px rgba(0,0,0,.35) `; this.panel.appendChild(t); setTimeout(() => t.remove(), ms); } copyLast(n=50) { const lines = log().getRecentLogs(n); if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(lines).then(() => { log().info(`Copied last ${n} lines to clipboard`); this.toast(`Copied last ${n} logs`); }).catch(e => this._fallbackCopy(lines, e)); } else { this._fallbackCopy(lines); } } _fallbackCopy(text, originalError = null) { try { const overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed;inset:0;z-index:999999;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;'; const panel = document.createElement('div'); panel.style.cssText = 'background:#1f2937;color:#e5e7eb;padding:12px 12px 8px;border-radius:8px;width:min(760px,90vw);max-height:70vh;display:flex;flex-direction:column;gap:8px;box-shadow:0 10px 30px rgba(0,0,0,0.5)'; const title = document.createElement('div'); title.textContent = 'Copy to clipboard'; title.style.cssText = 'font:600 14px system-ui,sans-serif;'; const hint = document.createElement('div'); hint.textContent = 'Press Ctrl+C (Windows/Linux) or ⌘+C (macOS) to copy the selected text.'; hint.style.cssText = 'font:12px system-ui,sans-serif;opacity:0.85;'; const ta = document.createElement('textarea'); ta.value = text; ta.readOnly = true; ta.style.cssText = 'width:100%;height:40vh;font:12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;'; const row = document.createElement('div'); row.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;'; const close = document.createElement('button'); close.textContent = 'Close'; close.style.cssText = 'padding:6px 10px;background:#374151;color:#e5e7eb;border:1px solid #4b5563;border-radius:6px;cursor:pointer;'; close.onclick = () => overlay.remove(); panel.append(title, hint, ta, row); row.append(close); overlay.append(panel); document.body.appendChild(overlay); ta.focus(); ta.select(); log().warn('Clipboard API unavailable; showing manual copy UI', { error: originalError?.message }); } catch (e) { log().warn('Clipboard copy failed', { error: originalError?.message || e.message }); } } mount() { if (!document.body) { setTimeout(() => this.mount(), 100); return; } if (!cfg().get('debug.showPanel')) return; const root = document.createElement('div'); root.style.cssText = ` position: fixed; ${this.panelState.left!==undefined ? `left:${this.panelState.left}px; top:${this.panelState.top}px;` : 'right:16px; bottom:16px;'} z-index: 2147483647; width: 460px; max-height: 55vh; display: flex; flex-direction: column; background: rgba(20,20,24,0.92); border:1px solid #3b3b46; border-radius: 8px; color:#e5e7eb; font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; box-shadow: 0 16px 40px rgba(0,0,0,0.55); backdrop-filter: blur(4px); `; root.innerHTML = `
AI Repo Commander
`; document.body.appendChild(root); this.panel = root; this.bodyLogs = root.querySelector('.rc-body-logs'); this.bodyTools = root.querySelector('.rc-body-tools'); this._wireControls(); this._startLogRefresh(); // Force an initial log to verify logging works setTimeout(() => { log().info('Debug panel mounted and logging active'); log().info(`Panel visible at: ${this.panelState.left !== undefined ? `(${this.panelState.left}, ${this.panelState.top})` : '(bottom-right)'}`); }, 100); } _wireControls() { const root = this.panel; // Log level selector const sel = root.querySelector('.rc-level'); const currentLevel = cfg().get('debug.level'); sel.value = String(currentLevel); log().trace(`[Debug Panel] Current log level: ${currentLevel}`); sel.addEventListener('change', () => { const newLevel = parseInt(sel.value, 10); log().setLevel(newLevel); log().info(`[Debug Panel] Log level changed to ${newLevel}`); }); // Copy buttons root.querySelector('.rc-copy').addEventListener('click', (e) => { this.copyLast(50); this.flashBtn(e.currentTarget, 'Copied'); }); root.querySelector('.rc-copy-200').addEventListener('click', (e) => { this.copyLast(200); this.flashBtn(e.currentTarget, 'Copied'); }); // Pause/Resume const pauseBtn = root.querySelector('.rc-pause'); pauseBtn.addEventListener('click', () => { const paused = !cfg().get('runtime.paused'); cfg().set('runtime.paused', paused); pauseBtn.textContent = paused ? 'Resume' : 'Pause'; pauseBtn.style.background = paused ? '#f59e0b' : ''; pauseBtn.style.color = paused ? '#111827' : ''; this.flashBtn(pauseBtn, paused ? 'Paused' : 'Resumed'); this.toast(paused ? 'Paused scanning' : 'Resumed scanning'); log().info(`Runtime ${paused ? 'paused' : 'resumed'}`); }); // Queue clear const queueBtn = root.querySelector('.rc-queue-clear'); queueBtn.addEventListener('click', (e) => { window.AI_REPO_QUEUE?.clear?.(); this.flashBtn(e.currentTarget, 'Cleared'); this.toast('Queue cleared'); log().warn('Command queue cleared'); }); // Emergency STOP root.querySelector('.rc-stop').addEventListener('click', (e) => { window.AI_REPO_STOP?.(); this.flashBtn(e.currentTarget, 'Stopped'); this.toast('Emergency STOP activated'); log().warn('Emergency STOP activated'); }); // Tabs const tabLogs = root.querySelector('.rc-tab-logs'); const tabTools = root.querySelector('.rc-tab-tools'); const selectTab = (tools=false) => { this.bodyLogs.style.display = tools ? 'none' : 'block'; this.bodyTools.style.display = tools ? 'block' : 'none'; tabLogs.style.background = tools ? '#111827' : '#1f2937'; tabTools.style.background = tools ? '#1f2937' : '#111827'; }; tabLogs.addEventListener('click', () => selectTab(false)); tabTools.addEventListener('click', () => { selectTab(true); this._loadToolsPanel(); }); // Collapse const collapseBtn = root.querySelector('.rc-collapse'); const setCollapsed = (c) => { this.collapsed = c; this.bodyLogs.style.display = c ? 'none' : 'block'; this.bodyTools.style.display = 'none'; collapseBtn.textContent = c ? '▸' : '▾'; this._savePanelState({ collapsed: c }); }; setCollapsed(!!this.panelState.collapsed); collapseBtn.addEventListener('click', () => setCollapsed(!this.collapsed)); // Dragging const header = root.querySelector('.rc-header'); header.addEventListener('mousedown', (e) => { const tgt = e.target instanceof Element ? e.target : e.target?.parentElement; if (tgt?.closest('button,select,input,textarea,label')) return; this.drag.active = true; const rect = root.getBoundingClientRect(); this.drag.dx = e.clientX - rect.left; this.drag.dy = e.clientY - rect.top; root.style.right = 'auto'; root.style.bottom = 'auto'; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }); const onMove = (e) => { if (!this.drag.active) return; const x = Math.max(0, Math.min(window.innerWidth - this.panel.offsetWidth, e.clientX - this.drag.dx)); const y = Math.max(0, Math.min(window.innerHeight - 40, e.clientY - this.drag.dy)); this.panel.style.left = `${x}px`; this.panel.style.top = `${y}px`; }; const onUp = () => { if (!this.drag.active) return; this.drag.active = false; this._savePanelState({ left: parseInt(this.panel.style.left||'0',10), top: parseInt(this.panel.style.top||'0',10) }); document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }; // Clear History root.querySelector('.rc-clear-history').addEventListener('click', (e) => { try { window.AI_REPO_HISTORY?.clear?.(); log().info('Conversation history cleared'); if (typeof GM_notification !== 'undefined') { GM_notification({ title: 'AI Repo Commander', text: 'Execution marks cleared', timeout: 2500 }); } } catch (err) { log().warn('Error clearing history', { error: String(err) }); } this.flashBtn(e.currentTarget, 'Cleared'); this.toast('Conversation marks cleared'); }); // Toggles root.querySelectorAll('.rc-toggle').forEach(inp => { const key = inp.dataset.key; inp.checked = !!cfg().get(key); inp.addEventListener('change', () => { cfg().set(key, !!inp.checked); this.toast(`${key} = ${cfg().get(key) ? 'on' : 'off'}`); log().info(`Config ${key} => ${cfg().get(key)}`); }); }); // Number inputs root.querySelectorAll('.rc-num').forEach(inp => { inp.value = String(cfg().get(inp.dataset.key) ?? ''); inp.addEventListener('change', () => { const v = parseInt(inp.value, 10); if (!Number.isNaN(v)) { cfg().set(inp.dataset.key, v); this.toast(`${inp.dataset.key} = ${v}`); log().info(`Config ${inp.dataset.key} => ${v}`); } }); }); // Bridge Key root.querySelector('.rc-save-bridge-key').addEventListener('click', (e) => { const input = root.querySelector('.rc-bridge-key'); const val = (input.value || '').trim(); if (val && !/^•+$/.test(val)) { cfg().set('api.bridgeKey', val); input.value = '•'.repeat(8); this.flashBtn(e.currentTarget, 'Saved'); this.toast('Bridge key saved'); log().info('Bridge key updated'); } else { this.toast('Invalid key', 1500); } }); root.querySelector('.rc-clear-bridge-key').addEventListener('click', (e) => { cfg().set('api.bridgeKey', ''); root.querySelector('.rc-bridge-key').value = ''; this.flashBtn(e.currentTarget, 'Cleared'); this.toast('Bridge key cleared'); log().info('Bridge key cleared'); }); // Config JSON root.querySelector('.rc-save-json').addEventListener('click', (e) => { try { const raw = root.querySelector('.rc-json').value; const parsed = JSON.parse(raw); // Don't override runtime/version from JSON delete parsed.meta; delete parsed.runtime; // Merge into current config Object.keys(parsed).forEach(section => { if (typeof parsed[section] === 'object' && !Array.isArray(parsed[section])) { Object.keys(parsed[section]).forEach(key => { cfg().set(`${section}.${key}`, parsed[section][key]); }); } }); this.flashBtn(e.currentTarget, 'Saved'); this.toast('Config saved'); log().info('Config JSON saved'); this._loadToolsPanel(); // Refresh } catch (err) { this.toast('Invalid JSON', 1500); log().warn('Invalid JSON in config textarea', { error: String(err) }); } }); root.querySelector('.rc-reset-defaults').addEventListener('click', (e) => { if (!confirm('Reset all settings to defaults? This will reload the page.')) return; localStorage.removeItem(STORAGE_KEYS.cfg); this.flashBtn(e.currentTarget, 'Reset'); this.toast('Resetting...', 1500); setTimeout(() => location.reload(), 1000); }); } _loadToolsPanel() { const root = this.panel; // Load toggle states root.querySelectorAll('.rc-toggle').forEach(inp => { inp.checked = !!cfg().get(inp.dataset.key); }); // Load number values root.querySelectorAll('.rc-num').forEach(inp => { inp.value = String(cfg().get(inp.dataset.key) ?? ''); }); // Load bridge key (masked) const bridgeKeyInput = root.querySelector('.rc-bridge-key'); const bridgeKey = cfg().get('api.bridgeKey'); if (bridgeKeyInput) { bridgeKeyInput.value = bridgeKey ? '•'.repeat(8) : ''; } // Load config JSON (sanitized) const dump = cfg().config; const sanitized = JSON.parse(JSON.stringify(dump)); if (sanitized.api && sanitized.api.bridgeKey) { sanitized.api.bridgeKey = '•'.repeat(8); } root.querySelector('.rc-json').value = JSON.stringify(sanitized, null, 2); } _startLogRefresh() { const renderLogs = () => { if (!this.bodyLogs || this.collapsed) return; const logger = log(); if (!logger || !logger.buffer) { this.bodyLogs.innerHTML = '
Logger not initialized yet...
'; return; } const rows = logger.buffer.slice(-80); if (rows.length === 0) { this.bodyLogs.innerHTML = '
No logs yet. Waiting for activity...
'; return; } this.bodyLogs.innerHTML = rows.map(e => `
${e.timestamp} ${e.level.padEnd(5)} ${e.message}${e.data ? ' ' + JSON.stringify(e.data) : ''}
` ).join(''); this.bodyLogs.scrollTop = this.bodyLogs.scrollHeight; }; setInterval(renderLogs, 1000); renderLogs(); } } const panel = new DebugPanel(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => panel.mount()); } else { panel.mount(); } window.AI_REPO_DEBUG_PANEL = panel; })(); // ==DEBUG PANEL END==