// ==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 = `
`;
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==