From c3fb382127b8477f8ca6abd9f45ccbac916c280d Mon Sep 17 00:00:00 2001 From: rob Date: Wed, 15 Oct 2025 21:04:47 -0300 Subject: [PATCH] included more features --- src/config.js | 31 ++- src/debug-panel.js | 529 ++++++++++++++++++++++++++++++++++++++++----- src/logger.js | 39 ++++ src/main.js | 12 + 4 files changed, 557 insertions(+), 54 deletions(-) diff --git a/src/config.js b/src/config.js index 52afefe..590c39d 100644 --- a/src/config.js +++ b/src/config.js @@ -19,6 +19,7 @@ debug: { enabled: true, level: 2, // 0=off, 1=errors, 2=info, 3=verbose, 4=trace + watchMs: 120000, maxLines: 400, showPanel: true }, @@ -27,13 +28,21 @@ debounceDelay: 6500, settleCheckMs: 1300, settlePollMs: 250, - requireTerminator: true + requireTerminator: true, + coldStartMs: 2000, + stuckAfterMs: 10 * 60 * 1000, + scanDebounceMs: 400, + fastWarnMs: 50, + slowWarnMs: 60000, + clusterRescanMs: 1000, + clusterMaxLookahead: 3 }, queue: { minDelayMs: 1500, maxPerMinute: 15, - maxPerMessage: 5 + maxPerMessage: 5, + waitForComposerMs: 12000 }, ui: { @@ -41,7 +50,23 @@ appendTrailingNewline: true, postPasteDelayMs: 600, showExecutedMarker: true, - processExisting: false // used by main.js + processExisting: false, + submitMode: 'button_first', + maxComposerWaitMs: 15 * 60 * 1000, + submitMaxRetries: 12 + }, + + storage: { + dedupeTtlMs: 30 * 24 * 60 * 60 * 1000, // 30 days + cleanupAfterMs: 30000, + cleanupIntervalMs: 60000 + }, + + response: { + bufferFlushDelayMs: 500, + sectionHeadings: true, + maxPasteChars: 250000, + splitLongResponses: true }, // Runtime state (not persisted) diff --git a/src/debug-panel.js b/src/debug-panel.js index f4ab468..4212540 100644 --- a/src/debug-panel.js +++ b/src/debug-panel.js @@ -1,69 +1,496 @@ // ==DEBUG PANEL START== -// Depends on: config.js, logger.js, queue.js +// Depends on: config.js, logger.js, queue.js, storage.js +/* 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.root = null; } - mount() { - if (!cfg().get('debug.showPanel')) return; - if (this.root) return; - const root = document.createElement('div'); - root.style.cssText = ` - position:fixed; right:16px; bottom:16px; z-index:2147483647; width:460px; max-height:55vh; - display:flex; flex-direction:column; background:rgba(20,20,24,.92); border:1px solid #3b3b46; border-radius:8px; - color:#e5e7eb; font:12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; box-shadow:0 16px 40px rgba(0,0,0,.55); - `; - root.innerHTML = ` -
- AI Repo Commander - - - -
-
- `; - document.body.appendChild(root); - this.root = root; - this.body = root.querySelector('[data-body]'); - this._wire(); - this._tick(); + 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(); } - _wire() { - this.root.addEventListener('click', (e) => { - const btn = e.target.closest('button[data-act]'); if (!btn) return; - const act = btn.getAttribute('data-act'); - if (act === 'copy') navigator.clipboard?.writeText(log().getRecentLogs(200)); - if (act === 'pause') { - const paused = !cfg().get('runtime.paused'); cfg().set('runtime.paused', paused); - btn.textContent = paused ? 'Resume' : 'Pause'; - log().info(paused ? 'Paused' : 'Resumed'); - } - if (act === 'clearq') window.AI_REPO_QUEUE.clear(); - }); - // queue badge - const q = window.AI_REPO_QUEUE; - if (q) q.onSizeChange = (n) => this._toast(`Queue: ${n}`); + + _loadPanelState() { + try { + return JSON.parse(localStorage.getItem(STORAGE_KEYS.panel) || '{}'); + } catch { return {}; } } - _toast(msg) { - if (!this.root) 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;'; - this.root.appendChild(t); setTimeout(() => t.remove(), 1000); + 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); } - _tick() { - if (!this.body) return; - const rows = window.AI_REPO_LOGGER?.buffer?.slice(-80) || []; - this.body.innerHTML = rows.map(e => `
${e.timestamp} ${e.level} ${e.message}
`).join(''); - requestAnimationFrame(() => this._tick()); + + 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(); + + log().info('Debug panel mounted'); + } + + _wireControls() { + const root = this.panel; + + // Log level selector + const sel = root.querySelector('.rc-level'); + sel.value = String(cfg().get('debug.level')); + sel.addEventListener('change', () => log().setLevel(parseInt(sel.value, 10))); + + // 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 rows = log().buffer.slice(-80); + 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(); + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => panel.mount()); + } else { + panel.mount(); + } window.AI_REPO_DEBUG_PANEL = panel; })(); diff --git a/src/logger.js b/src/logger.js index 4234085..8ac2642 100644 --- a/src/logger.js +++ b/src/logger.js @@ -4,13 +4,46 @@ constructor() { this.config = window.AI_REPO_CONFIG; this.buffer = []; + this.loopCounts = new Map(); + this.startedAt = Date.now(); + + // Cleanup loop counters periodically + setInterval(() => { + const watchMs = this.config.get('debug.watchMs') || 120000; + if (Date.now() - this.startedAt > watchMs * 2) { + this.loopCounts.clear(); + this.startedAt = Date.now(); + } + }, this.config.get('debug.watchMs') || 120000); } + error(msg, data) { this._log(1, 'ERROR', msg, data); } warn(msg, data) { this._log(2, 'WARN', msg, data); } info(msg, data) { this._log(3, 'INFO', msg, data); } verbose(msg, data) { this._log(4, 'VERBOSE', msg, data); } trace(msg, data) { this._log(5, 'TRACE', msg, data); } + command(action, status, extra={}) { + const icon = { + detected:'👁️', parsing:'📝', validating:'✓', debouncing:'⏳', + executing:'⚙️', complete:'✅', error:'❌' + }[status] || '•'; + this.info(`${icon} ${action} [${status}]`, extra); + } + + logLoop(kind, msg) { + const k = `${kind}:${msg}`; + const cur = this.loopCounts.get(k) || 0; + const withinWatch = Date.now() - this.startedAt <= (this.config.get('debug.watchMs') || 120000); + if (!withinWatch && kind !== 'WARN') return; + if (cur >= 10) return; + this.loopCounts.set(k, cur + 1); + const suffix = (cur + 1) > 1 ? ` (${cur + 1}x)` : ''; + if (kind === 'ERROR') this.error(`${msg}${suffix}`); + else if (kind === 'WARN') this.warn(`${msg}${suffix}`); + else this.info(`${msg}${suffix}`); + } + _log(levelNum, levelName, msg, data) { const enabled = !!this.config.get('debug.enabled'); const level = this.config.get('debug.level') ?? 0; @@ -50,6 +83,12 @@ `${e.timestamp} ${e.level.padEnd(7)} ${e.message}${e.data ? ' ' + JSON.stringify(e.data) : ''}` ).join('\n'); } + + setLevel(n) { + const lv = Math.max(0, Math.min(4, n)); + this.config.set('debug.level', lv); + this.info(`Log level => ${lv}`); + } } window.AI_REPO_LOGGER = new Logger(); diff --git a/src/main.js b/src/main.js index 9d102e9..fa1cc1b 100644 --- a/src/main.js +++ b/src/main.js @@ -135,6 +135,18 @@ resume: () => { config.set('runtime.paused', false); logger.info('Resumed'); }, clearHistory: () => { history.clear(); logger.info('History cleared'); } }; + + // Emergency STOP function + window.AI_REPO_STOP = () => { + config.set('api.enabled', false); + config.set('runtime.paused', true); + + const queuedCount = window.AI_REPO_QUEUE?.size?.() || 0; + window.AI_REPO_QUEUE?.clear?.(); + + logger.error(`🚨 EMERGENCY STOP: cancelled ${queuedCount} queued command(s)`); + logger.error('API disabled and scanning paused'); + }; } delay(ms) { return new Promise(r => setTimeout(r, ms)); }