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 = `
+
+
+
+ `;
+ 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)); }