Update src/ai-repo-commander.user.js
Debug threshold clarified & correct logLoop() logs at a visible level (INFO by default) Cleanup summary logs at INFO Console output is suppressed unless DEBUG_LEVEL ≥ 3 (verbose/trace) Clipboard copy has a safe fallback Pause button shows a clear visual state Panel mount is resilient if document.body isn’t ready Emergency STOP also clears the cleanup interval Initial scan explicitly skipped when PROCESS_EXISTING: false Plus the earlier low-risk hardening (require action:; pre-mark history)
This commit is contained in:
parent
e75a06a751
commit
bd53e289cf
|
|
@ -1,8 +1,8 @@
|
|||
// ==UserScript==
|
||||
// @name AI Repo Commander
|
||||
// @namespace http://tampermonkey.net/
|
||||
// @version 1.3.0
|
||||
// @description Execute ^%$bridge YAML commands from AI assistants in code blocks with persistent dedupe, robust paste, and optional auto-submit
|
||||
// @version 1.3.2
|
||||
// @description Execute ^%$bridge YAML commands from AI assistants in code blocks with persistent dedupe, robust paste, optional auto-submit, and a built-in debug console
|
||||
// @author Your Name
|
||||
// @match https://chat.openai.com/*
|
||||
// @match https://chatgpt.com/*
|
||||
|
|
@ -19,14 +19,18 @@
|
|||
|
||||
// ---------------------- Config ----------------------
|
||||
const CONFIG = {
|
||||
ENABLE_API: true, // Master kill switch
|
||||
DEBUG_MODE: true, // Console logs
|
||||
ENABLE_API: true, // Master kill switch (STOP API flips this to false)
|
||||
DEBUG_MODE: true, // Global on/off for debug logging
|
||||
DEBUG_LEVEL: 2, // 0=off, 1=errors, 2=info, 3=verbose, 4=trace
|
||||
DEBUG_WATCH_MS: 120000, // Only log tight loop spam for the first 2 minutes
|
||||
DEBUG_MAX_LINES: 400, // In-memory + panel lines
|
||||
DEBUG_SHOW_PANEL: true, // Show floating debug console UI
|
||||
DEBOUNCE_DELAY: 5000, // Bot typing protection
|
||||
MAX_RETRIES: 2, // Retry attempts (=> up to MAX_RETRIES+1 total tries)
|
||||
VERSION: '1.3.0',
|
||||
VERSION: '1.3.2',
|
||||
|
||||
PROCESS_EXISTING: false, // If false, only process messages added after init (but see initial delayed scan)
|
||||
ASSISTANT_ONLY: true, // Process assistant messages by default (your core use case)
|
||||
PROCESS_EXISTING: false, // If false, only process *new* messages (no initial rescan)
|
||||
ASSISTANT_ONLY: true, // Process assistant messages by default (core use case)
|
||||
|
||||
// Persistent dedupe window
|
||||
DEDUPE_TTL_MS: 30 * 24 * 60 * 60 * 1000, // 30 days
|
||||
|
|
@ -38,10 +42,195 @@
|
|||
// Paste + submit behavior
|
||||
APPEND_TRAILING_NEWLINE: true, // Add '\n' after pasted text
|
||||
AUTO_SUBMIT: true, // Try to submit after pasting content
|
||||
POST_PASTE_DELAY_MS: 250, // Small delay before submit to let editors settle
|
||||
POST_PASTE_DELAY_MS: 250, // Delay before submit to let editors settle
|
||||
SUBMIT_MODE: 'button_first', // 'button_first' | 'enter_only' | 'smart'
|
||||
|
||||
// Runtime toggles (live-updated by the debug panel)
|
||||
RUNTIME: {
|
||||
PAUSED: false, // Pause scanning + execution via panel
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------- Debug Console ----------------------
|
||||
let RC_DEBUG = null;
|
||||
|
||||
class DebugConsole {
|
||||
constructor(cfg) {
|
||||
this.cfg = cfg;
|
||||
this.buf = [];
|
||||
this.loopCounts = new Map();
|
||||
this.startedAt = Date.now();
|
||||
this.panel = null;
|
||||
if (cfg.DEBUG_SHOW_PANEL) this.mount();
|
||||
this.info(`Debug console ready (level=${cfg.DEBUG_LEVEL})`);
|
||||
}
|
||||
// Levels: 1=ERROR, 2=WARN, 3=INFO, 4=VERB, 5=TRACE
|
||||
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, 'VERB', 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);
|
||||
}
|
||||
|
||||
nowIso() { return new Date().toISOString(); }
|
||||
withinWatch() { return Date.now() - this.startedAt <= this.cfg.DEBUG_WATCH_MS; }
|
||||
|
||||
// Loop/ticker messages → suppress after 10 repeats or after WATCH window
|
||||
logLoop(kind, msg) {
|
||||
const k = `${kind}:${msg}`;
|
||||
const cur = this.loopCounts.get(k) || 0;
|
||||
if (!this.withinWatch() && kind !== 'WARN') return;
|
||||
if (cur >= 10) return;
|
||||
this.loopCounts.set(k, cur + 1);
|
||||
const suffix = (cur + 1) > 1 ? ` (${cur + 1}x)` : '';
|
||||
// default to INFO (visible at level 2+)
|
||||
if (kind === 'ERROR') this.error(`${msg}${suffix}`);
|
||||
else if (kind === 'WARN') this.warn(`${msg}${suffix}`);
|
||||
else this.info(`${msg}${suffix}`);
|
||||
}
|
||||
|
||||
copyLast(n=50) {
|
||||
const lines = this.buf.slice(-n).map(e => `${e.ts} ${e.level.padEnd(5)} ${e.msg}${e.data? ' ' + JSON.stringify(e.data): ''}`).join('\n');
|
||||
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(lines).then(() => {
|
||||
this.info(`Copied last ${Math.min(n, this.buf.length)} lines to clipboard`);
|
||||
}).catch(e => this._fallbackCopy(lines, e));
|
||||
} else {
|
||||
this._fallbackCopy(lines);
|
||||
}
|
||||
}
|
||||
|
||||
_fallbackCopy(text, originalError = null) {
|
||||
try {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.opacity = '0';
|
||||
document.body.appendChild(ta);
|
||||
ta.focus();
|
||||
ta.select();
|
||||
const ok = document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
if (ok) this.info(`Copied last ${text.split('\n').length} lines to clipboard (fallback)`);
|
||||
else this.warn('Clipboard copy failed (fallback)');
|
||||
} catch (e) {
|
||||
this.warn('Clipboard copy failed', { error: originalError?.message || e.message });
|
||||
}
|
||||
}
|
||||
|
||||
setLevel(n) {
|
||||
const lv = Math.max(0, Math.min(4, n)); // clamp 0..4
|
||||
this.cfg.DEBUG_LEVEL = lv;
|
||||
this.info(`Log level => ${lv}`);
|
||||
}
|
||||
|
||||
_sanitize(data) {
|
||||
if (!data) return null;
|
||||
try {
|
||||
if (data instanceof HTMLElement) return '[HTMLElement]';
|
||||
if (typeof data === 'string' && data.length > 400) return data.slice(0,400)+'…';
|
||||
if (typeof data === 'object') {
|
||||
const clone = { ...data };
|
||||
if (clone.element instanceof HTMLElement) clone.element = '[HTMLElement]';
|
||||
return clone;
|
||||
}
|
||||
} catch {}
|
||||
return data;
|
||||
}
|
||||
|
||||
_log(numericLevel, levelName, msg, data) {
|
||||
if (!this.cfg.DEBUG_MODE) return;
|
||||
|
||||
// Threshold map: 0=off, 1=ERROR, 2=+WARN+INFO, 3=+VERB, 4=+TRACE
|
||||
const thresholdMap = { 0: 0, 1: 1, 2: 3, 3: 4, 4: 5 };
|
||||
const threshold = thresholdMap[this.cfg.DEBUG_LEVEL] ?? 0;
|
||||
if (numericLevel > threshold) return;
|
||||
|
||||
const entry = { ts: this.nowIso(), level: levelName, msg: String(msg), data: this._sanitize(data) };
|
||||
this.buf.push(entry);
|
||||
if (this.buf.length > this.cfg.DEBUG_MAX_LINES) this.buf.splice(0, this.buf.length - this.cfg.DEBUG_MAX_LINES);
|
||||
|
||||
// Keep console quiet unless verbose+ is enabled
|
||||
if (this.cfg.DEBUG_LEVEL >= 3) {
|
||||
const prefix = `[AI RC]`;
|
||||
if (entry.data != null) console.log(prefix, entry.level, entry.msg, entry.data);
|
||||
else console.log(prefix, entry.level, entry.msg);
|
||||
}
|
||||
|
||||
if (this.panel) this._renderRow(entry);
|
||||
}
|
||||
|
||||
mount() {
|
||||
if (!document.body) {
|
||||
setTimeout(() => this.mount(), 100);
|
||||
return;
|
||||
}
|
||||
const root = document.createElement('div');
|
||||
root.style.cssText = `
|
||||
position: fixed; right: 16px; bottom: 16px; z-index: 2147483647;
|
||||
width: 420px; max-height: 45vh; 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 = `
|
||||
<div style="display:flex; gap:8px; align-items:center; padding:8px; border-bottom:1px solid #2c2c33">
|
||||
<strong style="flex:1">AI Repo Commander — Debug</strong>
|
||||
<label style="display:flex;align-items:center;gap:4px;">Level
|
||||
<select class="rc-level" style="background:#111827;color:#e5e7eb;border:1px solid #374151;border-radius:4px;padding:2px 6px;">
|
||||
<option value="0">off</option>
|
||||
<option value="1">errors</option>
|
||||
<option value="2" selected>info</option>
|
||||
<option value="3">verbose</option>
|
||||
<option value="4">trace</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="rc-copy" title="Copy last 50 lines" style="padding:4px 6px;">Copy 50</button>
|
||||
<button class="rc-pause" title="Pause/resume scanning" style="padding:4px 6px;">Pause</button>
|
||||
<button class="rc-stop" title="Stop API calls" style="padding:4px 6px;background:#7f1d1d;color:#fff;border:1px solid #991b1b">STOP API</button>
|
||||
</div>
|
||||
<div class="rc-body" style="overflow:auto; padding:8px; display:block; flex:1"></div>
|
||||
`;
|
||||
document.body.appendChild(root);
|
||||
this.panel = root;
|
||||
|
||||
const sel = root.querySelector('.rc-level');
|
||||
sel.value = String(this.cfg.DEBUG_LEVEL);
|
||||
sel.addEventListener('change', () => this.setLevel(parseInt(sel.value,10)));
|
||||
|
||||
root.querySelector('.rc-copy').addEventListener('click', () => this.copyLast(50));
|
||||
|
||||
const pauseBtn = root.querySelector('.rc-pause');
|
||||
pauseBtn.addEventListener('click', () => {
|
||||
this.cfg.RUNTIME.PAUSED = !this.cfg.RUNTIME.PAUSED;
|
||||
pauseBtn.textContent = this.cfg.RUNTIME.PAUSED ? 'Resume' : 'Pause';
|
||||
pauseBtn.style.background = this.cfg.RUNTIME.PAUSED ? '#f59e0b' : '';
|
||||
pauseBtn.style.color = this.cfg.RUNTIME.PAUSED ? '#111827' : '';
|
||||
this.info(`Runtime ${this.cfg.RUNTIME.PAUSED ? 'paused' : 'resumed'}`);
|
||||
});
|
||||
|
||||
root.querySelector('.rc-stop').addEventListener('click', () => {
|
||||
window.AI_REPO_STOP?.();
|
||||
this.warn('Emergency STOP activated');
|
||||
});
|
||||
}
|
||||
|
||||
_renderRow(e) {
|
||||
const body = this.panel.querySelector('.rc-body');
|
||||
const row = document.createElement('div');
|
||||
row.style.cssText = 'padding:4px 0;border-bottom:1px dashed #2a2a34;white-space:pre-wrap;word-break:break-word;';
|
||||
row.textContent = `${e.ts} ${e.level.padEnd(5)} ${e.msg}${e.data? ' ' + JSON.stringify(e.data): ''}`;
|
||||
body.appendChild(row);
|
||||
while (body.children.length > this.cfg.DEBUG_MAX_LINES) body.firstChild.remove();
|
||||
body.scrollTop = body.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------- Platform selectors ----------------------
|
||||
const PLATFORM_SELECTORS = {
|
||||
'chat.openai.com': { messages: '[data-message-author-role]', input: '#prompt-textarea, textarea, [contenteditable="true"]', content: '.markdown' },
|
||||
|
|
@ -294,7 +483,7 @@
|
|||
return false;
|
||||
|
||||
} catch (e) {
|
||||
console.warn('[AI Repo Commander] pasteToComposer failed:', e);
|
||||
RC_DEBUG?.warn('pasteToComposer failed', { error: String(e) });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -332,7 +521,6 @@
|
|||
}
|
||||
|
||||
static extractCommandBlock(text) {
|
||||
// Find ^%$bridge anywhere in the code block, prefer explicit '---' terminator
|
||||
const patterns = [
|
||||
/^\s*\^%\$bridge[ \t]*\n([\s\S]*?)\n---[ \t]*(?:\n|$)/m,
|
||||
/^\s*\^%\$bridge[ \t]*\n([\s\S]*?)(?=\n\s*$|\n---|\n```|$)/m,
|
||||
|
|
@ -480,10 +668,8 @@
|
|||
|
||||
static _extractFilesArray(payload) {
|
||||
const obj = Array.isArray(payload) ? payload[0] : payload;
|
||||
// Common shapes: { result: { files: [...] } } or { files: [...] }
|
||||
let files = obj?.result?.files ?? obj?.files ?? null;
|
||||
if (!files) {
|
||||
// Try to sniff: look for any array under result that looks like files
|
||||
const res = obj?.result;
|
||||
if (res) {
|
||||
for (const [k, v] of Object.entries(res)) {
|
||||
|
|
@ -497,7 +683,6 @@
|
|||
}
|
||||
|
||||
static _formatFilesListing(files) {
|
||||
// Accept strings or objects; prefer .path, else join directory/name
|
||||
const pickPath = (f) => {
|
||||
if (typeof f === 'string') return f;
|
||||
if (typeof f?.path === 'string') return f.path;
|
||||
|
|
@ -507,7 +692,6 @@
|
|||
};
|
||||
|
||||
const lines = files.map(pickPath).filter(Boolean).sort();
|
||||
// Friendly text block (fits most chat UIs)
|
||||
return '```text\n' + lines.join('\n') + '\n```';
|
||||
}
|
||||
|
||||
|
|
@ -521,7 +705,6 @@
|
|||
details: data.message || 'Operation completed successfully'
|
||||
});
|
||||
|
||||
// Auto-paste handlers
|
||||
if (command.action === 'get_file') {
|
||||
const body = this._extractGetFileBody(data);
|
||||
if (typeof body === 'string' && body.length) {
|
||||
|
|
@ -537,7 +720,6 @@
|
|||
const listing = this._formatFilesListing(files);
|
||||
await pasteAndMaybeSubmit(listing);
|
||||
} else {
|
||||
// Fallback: paste the whole payload as JSON for visibility
|
||||
const fallback = '```json\n' + JSON.stringify(data, null, 2) + '\n```';
|
||||
await pasteAndMaybeSubmit(fallback);
|
||||
GM_notification({ title: 'AI Repo Commander', text: 'list_files succeeded, but response had no obvious files array. Pasted raw JSON.', timeout: 5000 });
|
||||
|
|
@ -571,22 +753,43 @@
|
|||
// ---------------------- Monitor ----------------------
|
||||
class CommandMonitor {
|
||||
constructor() {
|
||||
this.trackedMessages = new Map(); // id -> { element, originalText, state, lastUpdate }
|
||||
this.trackedMessages = new Map(); // id -> { element, originalText, state, lastUpdate, startTime }
|
||||
this.history = new CommandHistory();
|
||||
this.observer = null;
|
||||
this.currentPlatform = null;
|
||||
this._idCounter = 0;
|
||||
this.cleanupIntervalId = null;
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
getReadableMessageId(element) {
|
||||
this._idCounter += 1;
|
||||
const id = `cmd-${this._idCounter}-${Math.random().toString(36).slice(2,6)}`;
|
||||
if (element?.dataset) element.dataset.aiRcId = id;
|
||||
return id;
|
||||
}
|
||||
|
||||
extractAction(text) {
|
||||
const m = /(^|\n)\s*action\s*:\s*([A-Za-z_][\w\-]*)/m.exec(text || '');
|
||||
return m ? m[2] : 'unknown';
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.detectPlatform();
|
||||
this.startObservation();
|
||||
this.setupEmergencyStop();
|
||||
this.log('AI Repo Commander initialized', CONFIG);
|
||||
RC_DEBUG?.info('AI Repo Commander initialized', {
|
||||
ENABLE_API: CONFIG.ENABLE_API,
|
||||
DEBUG_MODE: CONFIG.DEBUG_MODE,
|
||||
DEBOUNCE_DELAY: CONFIG.DEBOUNCE_DELAY,
|
||||
MAX_RETRIES: CONFIG.MAX_RETRIES,
|
||||
VERSION: CONFIG.VERSION
|
||||
});
|
||||
if (CONFIG.ENABLE_API) {
|
||||
console.warn('[AI Repo Commander] API is enabled — you will be prompted for your bridge key on first command.');
|
||||
RC_DEBUG?.warn('API is enabled — you will be prompted for your bridge key on first command.');
|
||||
}
|
||||
setInterval(() => this.cleanupProcessedCommands(), CONFIG.CLEANUP_INTERVAL_MS);
|
||||
// store interval id so STOP can clear it
|
||||
this.cleanupIntervalId = setInterval(() => this.cleanupProcessedCommands(), CONFIG.CLEANUP_INTERVAL_MS);
|
||||
}
|
||||
|
||||
detectPlatform() {
|
||||
|
|
@ -595,30 +798,68 @@
|
|||
}
|
||||
|
||||
startObservation() {
|
||||
// Throttled observer; only rescan if code blocks likely appeared
|
||||
let scanPending = false;
|
||||
const scheduleScan = () => {
|
||||
if (scanPending) return;
|
||||
scanPending = true;
|
||||
setTimeout(() => { scanPending = false; this.scanMessages(); }, 100);
|
||||
};
|
||||
|
||||
this.observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((m) => {
|
||||
m.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === 1) this.scanNode(node);
|
||||
});
|
||||
});
|
||||
for (const m of mutations) {
|
||||
for (const node of m.addedNodes) {
|
||||
if (node.nodeType !== 1) continue;
|
||||
if (node.matches?.('pre, code') || node.querySelector?.('pre, code')) {
|
||||
scheduleScan();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
this.observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// Always do one delayed scan to catch initial render, even with PROCESS_EXISTING=false
|
||||
setTimeout(() => {
|
||||
this.log('Initial scan after page load');
|
||||
this.scanMessages();
|
||||
}, 2000);
|
||||
|
||||
// Respect PROCESS_EXISTING on initial scan
|
||||
if (CONFIG.PROCESS_EXISTING) {
|
||||
setTimeout(() => {
|
||||
this.log('Deep scan of existing messages (PROCESS_EXISTING=true)');
|
||||
RC_DEBUG?.info('Initial scan after page load (PROCESS_EXISTING=true)');
|
||||
this.scanMessages();
|
||||
}, 5000);
|
||||
}, 1000);
|
||||
} else {
|
||||
RC_DEBUG?.info('Initial scan skipped (PROCESS_EXISTING=false)');
|
||||
}
|
||||
}
|
||||
|
||||
scanNode() { this.scanMessages(); }
|
||||
scanMessages() {
|
||||
if (CONFIG.RUNTIME.PAUSED) { RC_DEBUG?.logLoop('loop', 'scan paused'); return; }
|
||||
|
||||
const messages = document.querySelectorAll(this.currentPlatform.messages);
|
||||
let skipped = 0, found = 0;
|
||||
|
||||
messages.forEach((el) => {
|
||||
if (!this.isAssistantMessage(el)) return;
|
||||
if (el.dataset.aiRcProcessed) return;
|
||||
|
||||
const hit = this.findCommandInCodeBlock(el);
|
||||
if (!hit) return;
|
||||
|
||||
const cmdText = hit.text;
|
||||
|
||||
if (this.history.has(cmdText)) {
|
||||
el.dataset.aiRcProcessed = '1';
|
||||
skipped++;
|
||||
return;
|
||||
}
|
||||
|
||||
el.dataset.aiRcProcessed = '1';
|
||||
const id = this.getReadableMessageId(el);
|
||||
this.trackMessage(el, cmdText, id);
|
||||
found++;
|
||||
});
|
||||
|
||||
if (skipped) RC_DEBUG?.logLoop('loop', `skipped already-executed (${skipped})`);
|
||||
if (found) RC_DEBUG?.info(`Found ${found} new command(s)`);
|
||||
}
|
||||
|
||||
isAssistantMessage(el) {
|
||||
if (!CONFIG.ASSISTANT_ONLY) return true;
|
||||
|
|
@ -634,11 +875,12 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
// **HARDENED**: require header + action: to avoid partials
|
||||
findCommandInCodeBlock(el) {
|
||||
const blocks = el.querySelectorAll('pre code, pre, code');
|
||||
for (const b of blocks) {
|
||||
const txt = (b.textContent || '').trim();
|
||||
if (/(^|\n)\s*\^%\$bridge\b/m.test(txt)) {
|
||||
if (/(^|\n)\s*\^%\$bridge\b/m.test(txt) && /(^|\n)\s*action\s*:/m.test(txt)) {
|
||||
return { blockElement: b, text: txt };
|
||||
}
|
||||
}
|
||||
|
|
@ -646,10 +888,8 @@
|
|||
}
|
||||
|
||||
getMessageId(element) {
|
||||
if (element.dataset && element.dataset.aiRcId) return element.dataset.aiRcId;
|
||||
const id = element.id || `${element.className}-${element.childElementCount}-${Date.now()}-${Math.random().toString(36).slice(2,6)}`;
|
||||
if (element.dataset) element.dataset.aiRcId = id;
|
||||
return id;
|
||||
// kept for compatibility
|
||||
return this.getReadableMessageId(element);
|
||||
}
|
||||
|
||||
reextractCommandText(element) {
|
||||
|
|
@ -657,29 +897,8 @@
|
|||
return hit ? hit.text : '';
|
||||
}
|
||||
|
||||
scanMessages() {
|
||||
const messages = document.querySelectorAll(this.currentPlatform.messages);
|
||||
messages.forEach((el) => {
|
||||
if (!this.isAssistantMessage(el)) return;
|
||||
if (el.dataset.aiRcProcessed) return;
|
||||
|
||||
const hit = this.findCommandInCodeBlock(el);
|
||||
if (!hit) return;
|
||||
|
||||
const cmdText = hit.text;
|
||||
|
||||
if (this.history.has(cmdText)) {
|
||||
this.log('Skipping already-executed command (persistent history)');
|
||||
return;
|
||||
}
|
||||
|
||||
el.dataset.aiRcProcessed = '1';
|
||||
this.trackMessage(el, cmdText, this.getMessageId(el));
|
||||
});
|
||||
}
|
||||
|
||||
trackMessage(element, text, messageId) {
|
||||
this.log('New command detected in code block:', { messageId, preview: text.substring(0, 120) });
|
||||
RC_DEBUG?.info('New command detected', { messageId, preview: text.substring(0, 120) });
|
||||
this.trackedMessages.set(messageId, {
|
||||
element, originalText: text, state: COMMAND_STATES.DETECTED, startTime: Date.now(), lastUpdate: Date.now()
|
||||
});
|
||||
|
|
@ -690,16 +909,21 @@
|
|||
updateState(messageId, state) {
|
||||
const msg = this.trackedMessages.get(messageId);
|
||||
if (!msg) return;
|
||||
const old = msg.state;
|
||||
msg.state = state;
|
||||
msg.lastUpdate = Date.now();
|
||||
this.trackedMessages.set(messageId, msg);
|
||||
this.log(`Message ${messageId} state updated to: ${state}`);
|
||||
RC_DEBUG?.command(this.extractAction(msg.originalText), state, {
|
||||
messageId, transition: `${old} -> ${state}`
|
||||
});
|
||||
}
|
||||
|
||||
async processCommand(messageId) {
|
||||
if (CONFIG.RUNTIME.PAUSED) { RC_DEBUG?.info('process paused, skipping', { messageId }); return; }
|
||||
const started = Date.now();
|
||||
try {
|
||||
const message = this.trackedMessages.get(messageId);
|
||||
if (!message) return;
|
||||
if (!message) { RC_DEBUG?.error('Message not found', { messageId }); return; }
|
||||
|
||||
let parsed = CommandParser.parseYAMLCommand(message.originalText);
|
||||
this.updateState(messageId, COMMAND_STATES.VALIDATING);
|
||||
|
|
@ -712,40 +936,40 @@
|
|||
await this.debounce();
|
||||
|
||||
const after = this.reextractCommandText(message.element);
|
||||
|
||||
if (!after) {
|
||||
this.log('Command removed during debounce - aborting');
|
||||
this.updateState(messageId, COMMAND_STATES.ERROR);
|
||||
return;
|
||||
}
|
||||
if (!after) { this.updateState(messageId, COMMAND_STATES.ERROR); return; }
|
||||
|
||||
if (after !== before) {
|
||||
this.log('Command changed during debounce - updating and re-debouncing once');
|
||||
RC_DEBUG?.info('Command changed during debounce (re-validate)', { messageId });
|
||||
message.originalText = after;
|
||||
await this.debounce();
|
||||
|
||||
const finalTxt = this.reextractCommandText(message.element);
|
||||
if (!finalTxt) {
|
||||
this.log('Command removed after re-debounce - aborting');
|
||||
this.updateState(messageId, COMMAND_STATES.ERROR);
|
||||
return;
|
||||
}
|
||||
if (!finalTxt) { this.updateState(messageId, COMMAND_STATES.ERROR); return; }
|
||||
message.originalText = finalTxt;
|
||||
parsed = CommandParser.parseYAMLCommand(finalTxt);
|
||||
validation = CommandParser.validateStructure(parsed);
|
||||
if (!validation.isValid) throw new Error(`Final validation failed: ${validation.errors.join(', ')}`);
|
||||
}
|
||||
|
||||
// **HARDENED**: pre-mark to avoid duplicate runs if DOM churns
|
||||
this.history.mark(message.originalText);
|
||||
|
||||
this.updateState(messageId, COMMAND_STATES.EXECUTING);
|
||||
await ExecutionManager.executeCommand(parsed, message.element);
|
||||
|
||||
this.history.mark(message.originalText);
|
||||
const duration = Date.now() - started;
|
||||
if (duration < 100) RC_DEBUG?.warn('Command completed suspiciously fast', { messageId, duration });
|
||||
if (duration > 30000) RC_DEBUG?.warn('Command took unusually long', { messageId, duration });
|
||||
|
||||
this.updateState(messageId, COMMAND_STATES.COMPLETE);
|
||||
|
||||
} catch (error) {
|
||||
this.log(`Command processing error: ${error.message}`);
|
||||
const duration = Date.now() - started;
|
||||
RC_DEBUG?.error(`Command processing error: ${error.message}`, { messageId, duration });
|
||||
this.updateState(messageId, COMMAND_STATES.ERROR);
|
||||
const message = this.trackedMessages.get(messageId);
|
||||
// Silent ignore for non-commands/partials to avoid noisy inline errors
|
||||
if (/No valid command block|Missing required field:\s*action/i.test(error.message)) return;
|
||||
if (message) {
|
||||
UIFeedback.appendStatus(message.element, 'ERROR', { action: 'Command', details: error.message });
|
||||
}
|
||||
|
|
@ -756,32 +980,52 @@
|
|||
|
||||
cleanupProcessedCommands() {
|
||||
const now = Date.now();
|
||||
let count = 0;
|
||||
for (const [id, msg] of this.trackedMessages.entries()) {
|
||||
if ((msg.state === COMMAND_STATES.COMPLETE || msg.state === COMMAND_STATES.ERROR) &&
|
||||
now - (msg.lastUpdate || now) > CONFIG.CLEANUP_AFTER_MS) {
|
||||
this.trackedMessages.delete(id);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count) RC_DEBUG?.info(`Cleaned ${count} processed entries`);
|
||||
}
|
||||
|
||||
stopAllProcessing() {
|
||||
this.trackedMessages.clear();
|
||||
if (this.observer) this.observer.disconnect();
|
||||
if (this.cleanupIntervalId) {
|
||||
clearInterval(this.cleanupIntervalId);
|
||||
this.cleanupIntervalId = null;
|
||||
}
|
||||
}
|
||||
|
||||
setupEmergencyStop() {
|
||||
window.AI_REPO_STOP = () => {
|
||||
// Critical: stop API + pause runtime + cancel inflight + clear interval
|
||||
CONFIG.ENABLE_API = false;
|
||||
CONFIG.RUNTIME.PAUSED = true;
|
||||
|
||||
for (const [id, msg] of this.trackedMessages.entries()) {
|
||||
if (msg.state === COMMAND_STATES.EXECUTING || msg.state === COMMAND_STATES.DEBOUNCING) {
|
||||
RC_DEBUG?.error('Emergency stop - cancelling command', { messageId: id });
|
||||
this.updateState(id, COMMAND_STATES.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
this.stopAllProcessing();
|
||||
this.log('EMERGENCY STOP ACTIVATED');
|
||||
GM_notification({ text: 'AI Repo Commander Emergency Stop Activated', title: 'Safety System', timeout: 5000 });
|
||||
RC_DEBUG?.error('🚨 EMERGENCY STOP ACTIVATED 🚨');
|
||||
GM_notification({ text: 'All command processing stopped', title: 'Emergency Stop', timeout: 5000 });
|
||||
};
|
||||
}
|
||||
|
||||
log(...args) { if (CONFIG.DEBUG_MODE) console.log('[AI Repo Commander]', ...args); }
|
||||
log(...args) {
|
||||
const [msg, data] = (typeof args[0] === 'string') ? [args[0], args[1]] : ['(log)', args];
|
||||
RC_DEBUG?.verbose(msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------- Test commands ----------------------
|
||||
// ---------------------- Test commands (unchanged) ----------------------
|
||||
const TEST_COMMANDS = {
|
||||
validUpdate:
|
||||
`\
|
||||
|
|
@ -821,6 +1065,8 @@ path: .
|
|||
// ---------------------- Init ----------------------
|
||||
let commandMonitor;
|
||||
function initializeRepoCommander() {
|
||||
if (!RC_DEBUG) RC_DEBUG = new DebugConsole(CONFIG);
|
||||
|
||||
if (!commandMonitor) {
|
||||
commandMonitor = new CommandMonitor();
|
||||
window.AI_REPO_COMMANDER = {
|
||||
|
|
@ -831,10 +1077,10 @@ path: .
|
|||
history: commandMonitor.history,
|
||||
submitComposer // expose for quick testing
|
||||
};
|
||||
console.log('AI Repo Commander fully initialized');
|
||||
console.log('API Enabled:', CONFIG.ENABLE_API);
|
||||
console.log('Test commands available in window.AI_REPO_COMMANDER.test');
|
||||
console.log('Reset history: window.AI_REPO_COMMANDER.history.reset() or AI_REPO_CLEAR_HISTORY()');
|
||||
RC_DEBUG?.info('AI Repo Commander fully initialized');
|
||||
RC_DEBUG?.info('API Enabled:', { value: CONFIG.ENABLE_API });
|
||||
RC_DEBUG?.info('Test commands available in window.AI_REPO_COMMANDER.test');
|
||||
RC_DEBUG?.info('Reset history with window.AI_REPO_COMMANDER.history.reset() or AI_REPO_CLEAR_HISTORY()');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue