Update src/ai-repo-commander.user.js
changed command history aproach. More resilient - Handles edge cases in long conversations and streaming responses Better cleanup - Won't leak memory from stuck operations Cancellable operations - Can be stopped mid-flight without errors Better debugging - Magic numbers replaced with named config values Improved UX - Error states now offer recovery options
This commit is contained in:
parent
3575dec2ee
commit
f4ebb492b7
|
|
@ -41,10 +41,14 @@
|
||||||
|
|
||||||
PROCESS_EXISTING: false,
|
PROCESS_EXISTING: false,
|
||||||
ASSISTANT_ONLY: true,
|
ASSISTANT_ONLY: true,
|
||||||
|
BRIDGE_KEY: '', // Var to store the bridge key
|
||||||
|
|
||||||
// Persistent dedupe window
|
// Persistent dedupe window
|
||||||
DEDUPE_TTL_MS: 30 * 24 * 60 * 60 * 1000, // 30 days
|
DEDUPE_TTL_MS: 30 * 24 * 60 * 60 * 1000, // 30 days
|
||||||
|
|
||||||
|
COLD_START_MS: 2000, // Optional: during first 2s after load, don't auto-run pre-existing messages
|
||||||
|
SHOW_EXECUTED_MARKER: true, // Add a green border on messages that executed
|
||||||
|
|
||||||
// Housekeeping
|
// Housekeeping
|
||||||
CLEANUP_AFTER_MS: 30000,
|
CLEANUP_AFTER_MS: 30000,
|
||||||
CLEANUP_INTERVAL_MS: 60000,
|
CLEANUP_INTERVAL_MS: 60000,
|
||||||
|
|
@ -61,7 +65,14 @@
|
||||||
SETTLE_POLL_MS: 200, // was 300
|
SETTLE_POLL_MS: 200, // was 300
|
||||||
|
|
||||||
// Runtime toggles
|
// Runtime toggles
|
||||||
RUNTIME: { PAUSED: false }
|
RUNTIME: { PAUSED: false },
|
||||||
|
|
||||||
|
// New additions for hardening
|
||||||
|
STUCK_AFTER_MS: 10 * 60 * 1000, // 10min: force cleanup stuck entries
|
||||||
|
SCAN_DEBOUNCE_MS: 250, // throttle MutationObserver scans
|
||||||
|
FAST_WARN_MS: 50, // warn if command completes suspiciously fast
|
||||||
|
SLOW_WARN_MS: 60_000, // warn if command takes >1min
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function loadSavedConfig() {
|
function loadSavedConfig() {
|
||||||
|
|
@ -292,6 +303,25 @@
|
||||||
API_TIMEOUT_MS <input class="rc-num" data-key="API_TIMEOUT_MS" type="number" min="10000" step="5000" style="width:120px;background:#0b1220;color:#e5e7eb;border:1px solid #374151;border-radius:4px;padding:2px 6px;">
|
API_TIMEOUT_MS <input class="rc-num" data-key="API_TIMEOUT_MS" type="number" min="10000" step="5000" style="width:120px;background:#0b1220;color:#e5e7eb;border:1px solid #374151;border-radius:4px;padding:2px 6px;">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="grid-column:1 / -1;">
|
||||||
|
<h4 style="margin:8px 0 6px 0;">Bridge Configuration</h4>
|
||||||
|
<label style="display:flex;align-items:center;gap:8px;margin:4px 0;">
|
||||||
|
Bridge Key:
|
||||||
|
<input class="rc-bridge-key" type="password"
|
||||||
|
style="flex:1;background:#0b1220;color:#e5e7eb;border:1px solid #374151;border-radius:4px;padding:4px 8px;"
|
||||||
|
placeholder="Enter your bridge key here">
|
||||||
|
</label>
|
||||||
|
<div style="margin-top:6px;">
|
||||||
|
<button class="rc-save-bridge-key"
|
||||||
|
style="padding:6px 8px;border:1px solid #374151;border-radius:6px;background:#1f2937;color:#e5e7eb;">
|
||||||
|
Save Bridge Key
|
||||||
|
</button>
|
||||||
|
<button class="rc-clear-bridge-key"
|
||||||
|
style="padding:6px 8px;border:1px solid #374151;border-radius:6px;background:#1f2937;color:#e5e7eb;margin-left:8px;">
|
||||||
|
Clear Bridge Key
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div style="grid-column:1 / -1;">
|
<div style="grid-column:1 / -1;">
|
||||||
<h4 style="margin:8px 0 6px 0;">Config JSON</h4>
|
<h4 style="margin:8px 0 6px 0;">Config JSON</h4>
|
||||||
<textarea class="rc-json" style="width:100%;height:140px;background:#0b1220;color:#e5e7eb;border:1px solid #374151;border-radius:6px;padding:6px;"></textarea>
|
<textarea class="rc-json" style="width:100%;height:140px;background:#0b1220;color:#e5e7eb;border:1px solid #374151;border-radius:6px;padding:6px;"></textarea>
|
||||||
|
|
@ -341,9 +371,10 @@
|
||||||
tabTools.style.background = tools ? '#1f2937' : '#111827';
|
tabTools.style.background = tools ? '#1f2937' : '#111827';
|
||||||
};
|
};
|
||||||
tabLogs.addEventListener('click', () => selectTab(false));
|
tabLogs.addEventListener('click', () => selectTab(false));
|
||||||
|
|
||||||
tabTools.addEventListener('click', () => {
|
tabTools.addEventListener('click', () => {
|
||||||
selectTab(true);
|
selectTab(true);
|
||||||
// refresh tools view values
|
// refresh toggles/nums
|
||||||
root.querySelectorAll('.rc-toggle').forEach(inp => {
|
root.querySelectorAll('.rc-toggle').forEach(inp => {
|
||||||
const key = inp.dataset.key;
|
const key = inp.dataset.key;
|
||||||
inp.checked = !!this.cfg[key];
|
inp.checked = !!this.cfg[key];
|
||||||
|
|
@ -351,7 +382,15 @@
|
||||||
root.querySelectorAll('.rc-num').forEach(inp => {
|
root.querySelectorAll('.rc-num').forEach(inp => {
|
||||||
inp.value = String(this.cfg[inp.dataset.key] ?? '');
|
inp.value = String(this.cfg[inp.dataset.key] ?? '');
|
||||||
});
|
});
|
||||||
root.querySelector('.rc-json').value = JSON.stringify(this.cfg, null, 2);
|
|
||||||
|
// Mask BRIDGE_KEY in JSON dump
|
||||||
|
const dump = JSON.parse(JSON.stringify(this.cfg));
|
||||||
|
if (dump.BRIDGE_KEY) dump.BRIDGE_KEY = '•'.repeat(8);
|
||||||
|
root.querySelector('.rc-json').value = JSON.stringify(dump, null, 2);
|
||||||
|
|
||||||
|
// Mask the bridge key input (never show the real key)
|
||||||
|
const bridgeKeyInput = root.querySelector('.rc-bridge-key');
|
||||||
|
if (bridgeKeyInput) bridgeKeyInput.value = this.cfg.BRIDGE_KEY ? '•'.repeat(8) : '';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Collapse
|
// Collapse
|
||||||
|
|
@ -395,11 +434,18 @@
|
||||||
|
|
||||||
// Tools: Clear History
|
// Tools: Clear History
|
||||||
root.querySelector('.rc-clear-history').addEventListener('click', () => {
|
root.querySelector('.rc-clear-history').addEventListener('click', () => {
|
||||||
localStorage.removeItem(STORAGE_KEYS.history);
|
try {
|
||||||
this.info('Command history cleared');
|
commandMonitor?.history?.resetAll?.();
|
||||||
GM_notification({ title: 'AI Repo Commander', text: 'Command history cleared', timeout: 2500 });
|
RC_DEBUG?.info('Conversation history cleared');
|
||||||
|
GM_notification({ title: 'AI Repo Commander', text: 'This conversation’s execution marks cleared', timeout: 2500 });
|
||||||
|
} catch {
|
||||||
|
// fallback: remove legacy
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.history);
|
||||||
|
RC_DEBUG?.info('Legacy history key cleared');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Tools: toggles & numbers
|
// Tools: toggles & numbers
|
||||||
root.querySelectorAll('.rc-toggle').forEach(inp => {
|
root.querySelectorAll('.rc-toggle').forEach(inp => {
|
||||||
const key = inp.dataset.key;
|
const key = inp.dataset.key;
|
||||||
|
|
@ -427,16 +473,39 @@
|
||||||
try {
|
try {
|
||||||
const raw = root.querySelector('.rc-json').value;
|
const raw = root.querySelector('.rc-json').value;
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
|
|
||||||
|
// Handle BRIDGE_KEY specially: ignore masked or empty strings,
|
||||||
|
// accept a real value, then remove it from parsed so Object.assign
|
||||||
|
// doesn’t stomp it later.
|
||||||
|
if (Object.prototype.hasOwnProperty.call(parsed, 'BRIDGE_KEY')) {
|
||||||
|
const v = (parsed.BRIDGE_KEY ?? '').toString().trim();
|
||||||
|
if (v && !/^•+$/.test(v)) {
|
||||||
|
this.cfg.BRIDGE_KEY = v;
|
||||||
|
BRIDGE_KEY = v;
|
||||||
|
}
|
||||||
|
delete parsed.BRIDGE_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(this.cfg, parsed);
|
Object.assign(this.cfg, parsed);
|
||||||
saveConfig(this.cfg);
|
saveConfig(this.cfg);
|
||||||
|
|
||||||
|
// Re-mask JSON view after save
|
||||||
|
const dump = JSON.parse(JSON.stringify(this.cfg));
|
||||||
|
if (dump.BRIDGE_KEY) dump.BRIDGE_KEY = '•'.repeat(8);
|
||||||
|
root.querySelector('.rc-json').value = JSON.stringify(dump, null, 2);
|
||||||
|
|
||||||
this.info('Config JSON saved');
|
this.info('Config JSON saved');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.warn('Invalid JSON in config textarea', { error: String(e) });
|
this.warn('Invalid JSON in config textarea', { error: String(e) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
root.querySelector('.rc-reset-defaults').addEventListener('click', () => {
|
root.querySelector('.rc-reset-defaults').addEventListener('click', () => {
|
||||||
Object.assign(this.cfg, structuredClone(DEFAULT_CONFIG));
|
Object.assign(this.cfg, structuredClone(DEFAULT_CONFIG));
|
||||||
saveConfig(this.cfg);
|
saveConfig(this.cfg);
|
||||||
|
BRIDGE_KEY = null; // <— add
|
||||||
|
const bridgeKeyInput = root.querySelector('.rc-bridge-key'); // <— add
|
||||||
|
if (bridgeKeyInput) bridgeKeyInput.value = ''; // <— add
|
||||||
this.info('Config reset to defaults');
|
this.info('Config reset to defaults');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -447,6 +516,36 @@
|
||||||
pauseBtn.style.color = '#111827';
|
pauseBtn.style.color = '#111827';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bridge Key handlers
|
||||||
|
const bridgeKeyInput = root.querySelector('.rc-bridge-key');
|
||||||
|
bridgeKeyInput.value = this.cfg.BRIDGE_KEY ? '•'.repeat(8) : '';
|
||||||
|
|
||||||
|
root.querySelector('.rc-save-bridge-key').addEventListener('click', () => {
|
||||||
|
const raw = (bridgeKeyInput.value || '').trim();
|
||||||
|
// If user clicked into a masked field and didn't change it, do nothing
|
||||||
|
if (/^•+$/.test(raw)) {
|
||||||
|
this.info('Bridge key unchanged');
|
||||||
|
GM_notification({ title: 'AI Repo Commander', text: 'Bridge key unchanged', timeout: 2000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.cfg.BRIDGE_KEY = raw;
|
||||||
|
saveConfig(this.cfg);
|
||||||
|
BRIDGE_KEY = raw || null; // set runtime immediately
|
||||||
|
// re-mask UI
|
||||||
|
bridgeKeyInput.value = this.cfg.BRIDGE_KEY ? '•'.repeat(8) : '';
|
||||||
|
this.info('Bridge key saved (masked)');
|
||||||
|
GM_notification({ title: 'AI Repo Commander', text: 'Bridge key saved', timeout: 2500 });
|
||||||
|
});
|
||||||
|
|
||||||
|
root.querySelector('.rc-clear-bridge-key').addEventListener('click', () => {
|
||||||
|
this.cfg.BRIDGE_KEY = '';
|
||||||
|
bridgeKeyInput.value = '';
|
||||||
|
saveConfig(this.cfg);
|
||||||
|
BRIDGE_KEY = null; // Clear the runtime key too
|
||||||
|
this.info('Bridge key cleared');
|
||||||
|
GM_notification({ title: 'AI Repo Commander', text: 'Bridge key cleared', timeout: 2500 });
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderRow(e) {
|
_renderRow(e) {
|
||||||
|
|
@ -472,6 +571,38 @@
|
||||||
'claude.ai': { messages: '.chat-message', input: '[contenteditable="true"]', content: '.content' },
|
'claude.ai': { messages: '.chat-message', input: '[contenteditable="true"]', content: '.content' },
|
||||||
'gemini.google.com': { messages: '.message-content', input: 'textarea, [contenteditable="true"]', content: '.message-text' }
|
'gemini.google.com': { messages: '.message-content', input: 'textarea, [contenteditable="true"]', content: '.message-text' }
|
||||||
};
|
};
|
||||||
|
// ---------------------- Conversation-Aware Element History ----------------------
|
||||||
|
|
||||||
|
function getConversationId() {
|
||||||
|
const host = location.hostname;
|
||||||
|
// ChatGPT / OpenAI
|
||||||
|
if (/chatgpt\.com|chat\.openai\.com/.test(host)) {
|
||||||
|
const m = location.pathname.match(/\/c\/([^/]+)/);
|
||||||
|
return `chatgpt:${m ? m[1] : location.pathname}`;
|
||||||
|
}
|
||||||
|
// Claude
|
||||||
|
if (/claude\.ai/.test(host)) {
|
||||||
|
const m = location.pathname.match(/\/thread\/([^/]+)/);
|
||||||
|
return `claude:${m ? m[1] : location.pathname}`;
|
||||||
|
}
|
||||||
|
// Gemini / others
|
||||||
|
return `${host}:${location.pathname || '/'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fingerprintElement(el) {
|
||||||
|
// Prefer platform IDs if available
|
||||||
|
const attrs = ['data-message-id','data-id','data-testid','id'];
|
||||||
|
for (const a of attrs) {
|
||||||
|
const v = el.getAttribute?.(a) || el.closest?.(`[${a}]`)?.getAttribute?.(a);
|
||||||
|
if (v) return `id:${a}:${v}`;
|
||||||
|
}
|
||||||
|
// Fallback: DOM position + short content hash
|
||||||
|
const nodes = Array.from(document.querySelectorAll('[data-message-author-role], .chat-message, .message-content'));
|
||||||
|
const idx = Math.max(0, nodes.indexOf(el));
|
||||||
|
const s = (el.textContent || '').slice(0, 256);
|
||||||
|
let h = 5381; for (let i=0;i<s.length;i++) h=((h<<5)+h) ^ s.charCodeAt(i);
|
||||||
|
return `pos:${idx}:h:${(h>>>0).toString(36)}`;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------- Command requirements ----------------------
|
// ---------------------- Command requirements ----------------------
|
||||||
const REQUIRED_FIELDS = {
|
const REQUIRED_FIELDS = {
|
||||||
|
|
@ -510,57 +641,109 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------- Persistent Command History ----------------------
|
// ---------------------- Persistent Command History ----------------------
|
||||||
class CommandHistory {
|
class ConvHistory {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.key = STORAGE_KEYS.history;
|
this.convId = getConversationId();
|
||||||
this.ttl = CONFIG.DEDUPE_TTL_MS;
|
this.key = `ai_rc:conv:${this.convId}:processed`;
|
||||||
this.cleanup();
|
this.session = new Set();
|
||||||
|
this.cache = this._load();
|
||||||
|
this._cleanupTTL(); // ← Add cleanup on init
|
||||||
}
|
}
|
||||||
|
|
||||||
_load() {
|
_load() {
|
||||||
try { return JSON.parse(localStorage.getItem(this.key) || '{}'); }
|
try {
|
||||||
catch { return {}; }
|
return JSON.parse(localStorage.getItem(this.key) || '{}');
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_save(db) { localStorage.setItem(this.key, JSON.stringify(db)); }
|
|
||||||
_hash(s) {
|
_save() {
|
||||||
let h = 5381;
|
try {
|
||||||
for (let i = 0; i < s.length; i++) h = ((h << 5) + h) ^ s.charCodeAt(i);
|
localStorage.setItem(this.key, JSON.stringify(this.cache));
|
||||||
return (h >>> 0).toString(36);
|
} catch {}
|
||||||
}
|
}
|
||||||
has(text) {
|
|
||||||
const db = this._load();
|
_cleanupTTL() {
|
||||||
const k = this._hash(text);
|
const ttl = CONFIG.DEDUPE_TTL_MS || (30 * 24 * 60 * 60 * 1000);
|
||||||
const ts = db[k];
|
|
||||||
return !!ts && (Date.now() - ts) < this.ttl;
|
|
||||||
}
|
|
||||||
mark(text) {
|
|
||||||
const db = this._load();
|
|
||||||
db[this._hash(text)] = Date.now();
|
|
||||||
this._save(db);
|
|
||||||
}
|
|
||||||
unmark(text) {
|
|
||||||
const db = this._load();
|
|
||||||
const k = this._hash(text);
|
|
||||||
if (k in db) { delete db[k]; this._save(db); }
|
|
||||||
}
|
|
||||||
cleanup() {
|
|
||||||
const db = this._load();
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
let dirty = false;
|
let dirty = false;
|
||||||
for (const [k, ts] of Object.entries(db)) {
|
|
||||||
if (!ts || (now - ts) >= this.ttl) { delete db[k]; dirty = true; }
|
for (const [fp, ts] of Object.entries(this.cache)) {
|
||||||
|
if (!ts || (now - ts) > ttl) {
|
||||||
|
delete this.cache[fp];
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (dirty) this._save(db);
|
|
||||||
|
if (dirty) this._save();
|
||||||
|
}
|
||||||
|
|
||||||
|
hasElement(el) {
|
||||||
|
const fp = fingerprintElement(el);
|
||||||
|
return this.session.has(fp) || (fp in this.cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
markElement(el) {
|
||||||
|
const fp = fingerprintElement(el);
|
||||||
|
this.session.add(fp);
|
||||||
|
this.cache[fp] = Date.now();
|
||||||
|
this._save();
|
||||||
|
|
||||||
|
if (CONFIG.SHOW_EXECUTED_MARKER) {
|
||||||
|
try {
|
||||||
|
el.style.borderLeft = '3px solid #10B981';
|
||||||
|
el.title = 'Command executed — use "Run again" to re-run';
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unmarkElement(el) {
|
||||||
|
const fp = fingerprintElement(el);
|
||||||
|
this.session.delete(fp);
|
||||||
|
if (fp in this.cache) {
|
||||||
|
delete this.cache[fp];
|
||||||
|
this._save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetAll() {
|
||||||
|
this.session.clear();
|
||||||
|
localStorage.removeItem(this.key);
|
||||||
|
this.cache = {};
|
||||||
}
|
}
|
||||||
reset() { localStorage.removeItem(this.key); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global helpers (stable)
|
// Global helpers (stable)
|
||||||
window.AI_REPO = {
|
window.AI_REPO = {
|
||||||
clearHistory: () => localStorage.removeItem(STORAGE_KEYS.history),
|
clearHistory: () => {
|
||||||
|
try { commandMonitor?.history?.resetAll?.(); } catch {}
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.history); // legacy
|
||||||
|
},
|
||||||
getConfig: () => structuredClone(CONFIG),
|
getConfig: () => structuredClone(CONFIG),
|
||||||
setConfig: (patch) => { Object.assign(CONFIG, patch||{}); saveConfig(CONFIG); },
|
setConfig: (patch) => { Object.assign(CONFIG, patch||{}); saveConfig(CONFIG); },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function attachRunAgainUI(containerEl, onRun) {
|
||||||
|
if (containerEl.querySelector('.ai-rc-rerun')) return;
|
||||||
|
const bar = document.createElement('div');
|
||||||
|
bar.className = 'ai-rc-rerun';
|
||||||
|
bar.style.cssText = 'margin:8px 0; display:flex; gap:8px; align-items:center;';
|
||||||
|
const msg = document.createElement('span');
|
||||||
|
msg.textContent = 'Already executed.';
|
||||||
|
msg.style.cssText = 'flex:1; font-size:13px; opacity:.9;';
|
||||||
|
const run = document.createElement('button');
|
||||||
|
run.textContent = 'Run again';
|
||||||
|
run.style.cssText = 'padding:4px 10px; border:1px solid #374151; border-radius:4px; background:#1f2937; color:#e5e7eb;';
|
||||||
|
const dismiss = document.createElement('button');
|
||||||
|
dismiss.textContent = 'Dismiss';
|
||||||
|
dismiss.style.cssText = 'padding:4px 10px; border:1px solid #374151; border-radius:4px; background:#111827; color:#9ca3af;';
|
||||||
|
run.onclick = onRun;
|
||||||
|
dismiss.onclick = () => bar.remove();
|
||||||
|
bar.append(msg, run, dismiss);
|
||||||
|
containerEl.appendChild(bar);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------- UI feedback ----------------------
|
// ---------------------- UI feedback ----------------------
|
||||||
class UIFeedback {
|
class UIFeedback {
|
||||||
static appendStatus(sourceElement, templateType, data) {
|
static appendStatus(sourceElement, templateType, data) {
|
||||||
|
|
@ -853,11 +1036,23 @@
|
||||||
headers: { 'X-Bridge-Key': bridgeKey, 'Content-Type': 'application/json' },
|
headers: { 'X-Bridge-Key': bridgeKey, 'Content-Type': 'application/json' },
|
||||||
data: JSON.stringify(command),
|
data: JSON.stringify(command),
|
||||||
timeout: CONFIG.API_TIMEOUT_MS || 60000,
|
timeout: CONFIG.API_TIMEOUT_MS || 60000,
|
||||||
onload: (response) => (response.status >= 200 && response.status < 300)
|
|
||||||
? resolve(response)
|
onload: (response) => {
|
||||||
: reject(new Error(`API Error ${response.status}: ${response.statusText}`)),
|
if (response.status >= 200 && response.status < 300) {
|
||||||
onerror: (error) => reject(new Error(`Network error: ${error}`)),
|
return resolve(response);
|
||||||
ontimeout: () => reject(new Error('API request timeout'))
|
}
|
||||||
|
const body = response.responseText ? ` body=${response.responseText.slice(0,300)}` : '';
|
||||||
|
reject(new Error(`API Error ${response.status}: ${response.statusText}${body}`));
|
||||||
|
},
|
||||||
|
|
||||||
|
onerror: (error) => {
|
||||||
|
const msg = (error && (error.error || error.message))
|
||||||
|
? (error.error || error.message)
|
||||||
|
: JSON.stringify(error ?? {});
|
||||||
|
reject(new Error(`Network error: ${msg}`));
|
||||||
|
},
|
||||||
|
|
||||||
|
ontimeout: () => reject(new Error(`API request timeout after ${CONFIG.API_TIMEOUT_MS}ms`))
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -961,19 +1156,58 @@
|
||||||
|
|
||||||
// ---------------------- Bridge Key ----------------------
|
// ---------------------- Bridge Key ----------------------
|
||||||
let BRIDGE_KEY = null;
|
let BRIDGE_KEY = null;
|
||||||
|
|
||||||
function requireBridgeKeyIfNeeded() {
|
function requireBridgeKeyIfNeeded() {
|
||||||
if (CONFIG.ENABLE_API && !BRIDGE_KEY) {
|
if (!CONFIG.ENABLE_API) return BRIDGE_KEY;
|
||||||
BRIDGE_KEY = prompt('[AI Repo Commander] Enter your bridge key for this session:');
|
|
||||||
if (!BRIDGE_KEY) throw new Error('Bridge key required when API is enabled.');
|
// 1) Try runtime
|
||||||
|
if (BRIDGE_KEY && typeof BRIDGE_KEY === 'string' && BRIDGE_KEY.length) {
|
||||||
|
return BRIDGE_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2) Try saved config
|
||||||
|
if (CONFIG.BRIDGE_KEY && typeof CONFIG.BRIDGE_KEY === 'string' && CONFIG.BRIDGE_KEY.length) {
|
||||||
|
BRIDGE_KEY = CONFIG.BRIDGE_KEY;
|
||||||
|
RC_DEBUG?.info('Using saved bridge key from config');
|
||||||
|
return BRIDGE_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Prompt fallback
|
||||||
|
const entered = prompt(
|
||||||
|
'[AI Repo Commander] Enter your bridge key for this session (or set it in Tools → Bridge Configuration to avoid this prompt):'
|
||||||
|
);
|
||||||
|
if (!entered) throw new Error('Bridge key required when API is enabled.');
|
||||||
|
|
||||||
|
BRIDGE_KEY = entered;
|
||||||
|
|
||||||
|
// Offer to save for next time
|
||||||
|
try {
|
||||||
|
if (confirm('Save this bridge key in Settings → Bridge Configuration to avoid future prompts?')) {
|
||||||
|
CONFIG.BRIDGE_KEY = BRIDGE_KEY;
|
||||||
|
saveConfig(CONFIG);
|
||||||
|
RC_DEBUG?.info('Bridge key saved to config');
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
|
||||||
return BRIDGE_KEY;
|
return BRIDGE_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optional: expose a safe setter for console use (won't log the key)
|
||||||
|
window.AI_REPO_SET_KEY = function setBridgeKey(k) {
|
||||||
|
BRIDGE_KEY = (k || '').trim() || null;
|
||||||
|
if (BRIDGE_KEY) {
|
||||||
|
RC_DEBUG?.info('Bridge key set for this session');
|
||||||
|
} else {
|
||||||
|
RC_DEBUG?.info('Bridge key cleared for this session');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ---------------------- Monitor (with streaming “settle” & complete-block check) ----------------------
|
// ---------------------- Monitor (with streaming “settle” & complete-block check) ----------------------
|
||||||
class CommandMonitor {
|
class CommandMonitor {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.trackedMessages = new Map();
|
this.trackedMessages = new Map();
|
||||||
this.history = new CommandHistory();
|
this.history = new ConvHistory();
|
||||||
|
this.coldStartUntil = Date.now() + (CONFIG.COLD_START_MS || 0); // optional
|
||||||
this.observer = null;
|
this.observer = null;
|
||||||
this.currentPlatform = null;
|
this.currentPlatform = null;
|
||||||
this._idCounter = 0;
|
this._idCounter = 0;
|
||||||
|
|
@ -1005,7 +1239,11 @@
|
||||||
VERSION: CONFIG.VERSION
|
VERSION: CONFIG.VERSION
|
||||||
});
|
});
|
||||||
if (CONFIG.ENABLE_API) {
|
if (CONFIG.ENABLE_API) {
|
||||||
RC_DEBUG?.warn('API is enabled — you will be prompted for your bridge key on first command.');
|
if (CONFIG.BRIDGE_KEY) {
|
||||||
|
RC_DEBUG?.info('API is enabled — using saved bridge key from config');
|
||||||
|
} else {
|
||||||
|
RC_DEBUG?.warn('API is enabled — you will be prompted for your bridge key on first command.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.cleanupIntervalId = setInterval(() => this.cleanupProcessedCommands(), CONFIG.CLEANUP_INTERVAL_MS);
|
this.cleanupIntervalId = setInterval(() => this.cleanupProcessedCommands(), CONFIG.CLEANUP_INTERVAL_MS);
|
||||||
}
|
}
|
||||||
|
|
@ -1017,10 +1255,17 @@
|
||||||
|
|
||||||
startObservation() {
|
startObservation() {
|
||||||
let scanPending = false;
|
let scanPending = false;
|
||||||
|
let lastScan = 0;
|
||||||
|
|
||||||
const scheduleScan = () => {
|
const scheduleScan = () => {
|
||||||
if (scanPending) return;
|
if (scanPending) return;
|
||||||
scanPending = true;
|
scanPending = true;
|
||||||
setTimeout(() => { scanPending = false; this.scanMessages(); }, 120);
|
const delay = Math.max(0, CONFIG.SCAN_DEBOUNCE_MS - (Date.now() - lastScan));
|
||||||
|
setTimeout(() => {
|
||||||
|
scanPending = false;
|
||||||
|
lastScan = Date.now();
|
||||||
|
this.scanMessages();
|
||||||
|
}, delay);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.observer = new MutationObserver((mutations) => {
|
this.observer = new MutationObserver((mutations) => {
|
||||||
|
|
@ -1029,18 +1274,19 @@
|
||||||
if (node.nodeType !== 1) continue;
|
if (node.nodeType !== 1) continue;
|
||||||
if (node.matches?.('pre, code') || node.querySelector?.('pre, code')) {
|
if (node.matches?.('pre, code') || node.querySelector?.('pre, code')) {
|
||||||
scheduleScan();
|
scheduleScan();
|
||||||
break;
|
return; // Early exit after scheduling
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.observer.observe(document.body, { childList: true, subtree: true });
|
this.observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
|
||||||
if (CONFIG.PROCESS_EXISTING) {
|
if (CONFIG.PROCESS_EXISTING) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
RC_DEBUG?.info('Initial scan after page load (PROCESS_EXISTING=true)');
|
RC_DEBUG?.info('Initial scan after page load (PROCESS_EXISTING=true)');
|
||||||
this.scanMessages();
|
this.scanMessages();
|
||||||
}, 1000);
|
}, 600);
|
||||||
} else {
|
} else {
|
||||||
RC_DEBUG?.info('Initial scan skipped (PROCESS_EXISTING=false)');
|
RC_DEBUG?.info('Initial scan skipped (PROCESS_EXISTING=false)');
|
||||||
}
|
}
|
||||||
|
|
@ -1066,31 +1312,11 @@
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForStableCompleteBlock(element, initialText) {
|
|
||||||
// After debounce, wait until the block remains unchanged for SETTLE_CHECK_MS, with terminator present.
|
|
||||||
const mustEnd = Date.now() + Math.max(0, CONFIG.SETTLE_CHECK_MS);
|
|
||||||
let last = initialText;
|
|
||||||
while (Date.now() < mustEnd) {
|
|
||||||
await ExecutionManager.delay(CONFIG.SETTLE_POLL_MS);
|
|
||||||
const hit = this.findCommandInCodeBlock(element);
|
|
||||||
const txt = hit ? hit.text : '';
|
|
||||||
if (!txt || !this.isCompleteCommandText(txt)) {
|
|
||||||
// streaming not done yet; extend window slightly
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (txt === last) {
|
|
||||||
// stable during this poll; keep looping until timeout to ensure stability window
|
|
||||||
} else {
|
|
||||||
last = txt; // changed; reset window by moving mustEnd forward a bit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// final extract
|
|
||||||
const finalHit = this.findCommandInCodeBlock(element);
|
|
||||||
return finalHit ? finalHit.text : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
scanMessages() {
|
scanMessages() {
|
||||||
if (CONFIG.RUNTIME.PAUSED) { RC_DEBUG?.logLoop('loop', 'scan paused'); return; }
|
if (CONFIG.RUNTIME.PAUSED) {
|
||||||
|
RC_DEBUG?.logLoop('loop', 'scan paused');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const messages = document.querySelectorAll(this.currentPlatform.messages);
|
const messages = document.querySelectorAll(this.currentPlatform.messages);
|
||||||
let skipped = 0, found = 0;
|
let skipped = 0, found = 0;
|
||||||
|
|
@ -1104,20 +1330,47 @@
|
||||||
|
|
||||||
const cmdText = hit.text;
|
const cmdText = hit.text;
|
||||||
|
|
||||||
if (this.history.has(cmdText)) {
|
// Check if we're in cold start OR if this specific message element was already executed
|
||||||
|
const withinColdStart = Date.now() < this.coldStartUntil;
|
||||||
|
const alreadyProcessed = this.history.hasElement(el);
|
||||||
|
|
||||||
|
// If cold start OR already processed → show "Run Again" button (don't auto-execute)
|
||||||
|
if (withinColdStart || alreadyProcessed) {
|
||||||
el.dataset.aiRcProcessed = '1';
|
el.dataset.aiRcProcessed = '1';
|
||||||
|
|
||||||
|
const reason = withinColdStart
|
||||||
|
? 'page load (cold start)'
|
||||||
|
: 'already executed in this conversation';
|
||||||
|
|
||||||
|
RC_DEBUG?.verbose(`Skipping command - ${reason}`, {
|
||||||
|
preview: cmdText.slice(0, 80)
|
||||||
|
});
|
||||||
|
|
||||||
|
attachRunAgainUI(el, () => {
|
||||||
|
el.dataset.aiRcProcessed = '1'; // ← Prevents scan loop double-enqueue
|
||||||
|
|
||||||
|
const id = this.getReadableMessageId(el);
|
||||||
|
const hit2 = this.findCommandInCodeBlock(el);
|
||||||
|
if (hit2) {
|
||||||
|
this.trackMessage(el, hit2.text, id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
skipped++;
|
skipped++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New message that hasn't been executed → auto-execute once
|
||||||
el.dataset.aiRcProcessed = '1';
|
el.dataset.aiRcProcessed = '1';
|
||||||
|
this.history.markElement(el);
|
||||||
|
|
||||||
const id = this.getReadableMessageId(el);
|
const id = this.getReadableMessageId(el);
|
||||||
this.trackMessage(el, cmdText, id);
|
this.trackMessage(el, cmdText, id);
|
||||||
found++;
|
found++;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (skipped) RC_DEBUG?.logLoop('loop', `skipped already-executed (${skipped})`);
|
if (skipped) RC_DEBUG?.info(`Skipped ${skipped} command(s) - Run Again buttons added`);
|
||||||
if (found) RC_DEBUG?.info(`Found ${found} new command(s)`);
|
if (found) RC_DEBUG?.info(`Auto-executing ${found} new command(s)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
isAssistantMessage(el) {
|
isAssistantMessage(el) {
|
||||||
|
|
@ -1137,12 +1390,86 @@
|
||||||
trackMessage(element, text, messageId) {
|
trackMessage(element, text, messageId) {
|
||||||
RC_DEBUG?.info('New command detected', { messageId, preview: text.substring(0, 120) });
|
RC_DEBUG?.info('New command detected', { messageId, preview: text.substring(0, 120) });
|
||||||
this.trackedMessages.set(messageId, {
|
this.trackedMessages.set(messageId, {
|
||||||
element, originalText: text, state: COMMAND_STATES.DETECTED, startTime: Date.now(), lastUpdate: Date.now()
|
element,
|
||||||
|
originalText: text,
|
||||||
|
state: COMMAND_STATES.DETECTED,
|
||||||
|
startTime: Date.now(),
|
||||||
|
lastUpdate: Date.now(),
|
||||||
|
cancelToken: { cancelled: false } // ← ADD THIS
|
||||||
});
|
});
|
||||||
this.updateState(messageId, COMMAND_STATES.PARSING);
|
this.updateState(messageId, COMMAND_STATES.PARSING);
|
||||||
this.processCommand(messageId);
|
this.processCommand(messageId);
|
||||||
}
|
}
|
||||||
|
// Debounce that checks cancellation
|
||||||
|
async debounceWithCancel(messageId) {
|
||||||
|
const start = Date.now();
|
||||||
|
const delay = CONFIG.DEBOUNCE_DELAY;
|
||||||
|
const checkInterval = 100; // check every 100ms
|
||||||
|
|
||||||
|
while (Date.now() - start < delay) {
|
||||||
|
const msg = this.trackedMessages.get(messageId);
|
||||||
|
if (!msg || msg.cancelToken?.cancelled) return;
|
||||||
|
|
||||||
|
// Update lastUpdate to prevent premature cleanup
|
||||||
|
msg.lastUpdate = Date.now();
|
||||||
|
this.trackedMessages.set(messageId, msg);
|
||||||
|
|
||||||
|
await ExecutionManager.delay(Math.min(checkInterval, delay - (Date.now() - start)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updated settle check with cancellation and lastUpdate bumps
|
||||||
|
async waitForStableCompleteBlock(element, initialText, messageId) {
|
||||||
|
let deadline = Date.now() + Math.max(0, CONFIG.SETTLE_CHECK_MS);
|
||||||
|
let last = initialText;
|
||||||
|
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
// Check cancellation
|
||||||
|
const rec = this.trackedMessages.get(messageId);
|
||||||
|
if (!rec || rec.cancelToken?.cancelled) {
|
||||||
|
RC_DEBUG?.warn('Settle cancelled', { messageId });
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update lastUpdate to prevent cleanup during long wait
|
||||||
|
rec.lastUpdate = Date.now();
|
||||||
|
this.trackedMessages.set(messageId, rec);
|
||||||
|
|
||||||
|
await ExecutionManager.delay(CONFIG.SETTLE_POLL_MS);
|
||||||
|
const hit = this.findCommandInCodeBlock(element);
|
||||||
|
const txt = hit ? hit.text : '';
|
||||||
|
|
||||||
|
if (!txt || !this.isCompleteCommandText(txt)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (txt === last) {
|
||||||
|
// stable; keep waiting
|
||||||
|
} else {
|
||||||
|
last = txt;
|
||||||
|
deadline = Date.now() + Math.max(0, CONFIG.SETTLE_CHECK_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalHit = this.findCommandInCodeBlock(element);
|
||||||
|
return finalHit ? finalHit.text : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry UI helper
|
||||||
|
attachRetryUI(element, messageId) {
|
||||||
|
if (element.querySelector('.ai-rc-rerun')) return;
|
||||||
|
|
||||||
|
attachRunAgainUI(element, () => {
|
||||||
|
element.dataset.aiRcProcessed = '1';
|
||||||
|
const hit = this.findCommandInCodeBlock(element);
|
||||||
|
if (hit) {
|
||||||
|
// Clear old entry and create new one
|
||||||
|
this.trackedMessages.delete(messageId);
|
||||||
|
const newId = this.getReadableMessageId(element);
|
||||||
|
this.trackMessage(element, hit.text, newId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
updateState(messageId, state) {
|
updateState(messageId, state) {
|
||||||
const msg = this.trackedMessages.get(messageId);
|
const msg = this.trackedMessages.get(messageId);
|
||||||
if (!msg) return;
|
if (!msg) return;
|
||||||
|
|
@ -1156,61 +1483,98 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async processCommand(messageId) {
|
async processCommand(messageId) {
|
||||||
if (CONFIG.RUNTIME.PAUSED) { RC_DEBUG?.info('process paused, skipping', { messageId }); return; }
|
if (CONFIG.RUNTIME.PAUSED) {
|
||||||
|
RC_DEBUG?.info('process paused, skipping', { messageId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
try {
|
try {
|
||||||
const message = this.trackedMessages.get(messageId);
|
const message = this.trackedMessages.get(messageId);
|
||||||
if (!message) { RC_DEBUG?.error('Message not found', { messageId }); return; }
|
if (!message) {
|
||||||
|
RC_DEBUG?.error('Message not found', { messageId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 1) Initial parse check (strict: must include ---)
|
// ← CHECK CANCELLATION
|
||||||
|
if (message.cancelToken?.cancelled) {
|
||||||
|
RC_DEBUG?.warn('Operation cancelled', { messageId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Parse
|
||||||
let parsed;
|
let parsed;
|
||||||
try {
|
try {
|
||||||
parsed = CommandParser.parseYAMLCommand(message.originalText);
|
parsed = CommandParser.parseYAMLCommand(message.originalText);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
RC_DEBUG?.error(`Command parsing failed: ${err.message}`, { messageId });
|
RC_DEBUG?.error(`Command parsing failed: ${err.message}`, { messageId });
|
||||||
this.updateState(messageId, COMMAND_STATES.ERROR);
|
this.updateState(messageId, COMMAND_STATES.ERROR);
|
||||||
if (/No complete \^%\$bridge/.test(err.message)) return; // silent for partials
|
if (/No complete \^%\$bridge/.test(err.message)) return;
|
||||||
|
|
||||||
|
// ← ADD RUN AGAIN ON ERRORS
|
||||||
|
this.attachRetryUI(message.element, messageId);
|
||||||
|
|
||||||
UIFeedback.appendStatus(message.element, 'ERROR', { action: 'Command', details: err.message });
|
UIFeedback.appendStatus(message.element, 'ERROR', { action: 'Command', details: err.message });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ← CHECK CANCELLATION
|
||||||
|
if (message.cancelToken?.cancelled) {
|
||||||
|
RC_DEBUG?.warn('Operation cancelled after parse', { messageId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 2) Validate
|
// 2) Validate
|
||||||
this.updateState(messageId, COMMAND_STATES.VALIDATING);
|
this.updateState(messageId, COMMAND_STATES.VALIDATING);
|
||||||
let validation = CommandParser.validateStructure(parsed);
|
let validation = CommandParser.validateStructure(parsed);
|
||||||
if (!validation.isValid) throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
|
if (!validation.isValid) {
|
||||||
|
this.attachRetryUI(message.element, messageId);
|
||||||
|
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
// 3) Debounce, then settle: ensure final text complete & stable
|
// 3) Debounce
|
||||||
this.updateState(messageId, COMMAND_STATES.DEBOUNCING);
|
this.updateState(messageId, COMMAND_STATES.DEBOUNCING);
|
||||||
const before = message.originalText;
|
const before = message.originalText;
|
||||||
await this.debounce();
|
await this.debounceWithCancel(messageId);
|
||||||
const stable = await this.waitForStableCompleteBlock(message.element, before);
|
|
||||||
if (!stable) { this.updateState(messageId, COMMAND_STATES.ERROR); return; }
|
// ← CHECK CANCELLATION
|
||||||
|
if (message.cancelToken?.cancelled) {
|
||||||
|
RC_DEBUG?.warn('Operation cancelled after debounce', { messageId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stable = await this.waitForStableCompleteBlock(message.element, before, messageId);
|
||||||
|
if (!stable) {
|
||||||
|
this.updateState(messageId, COMMAND_STATES.ERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (stable !== before) {
|
if (stable !== before) {
|
||||||
RC_DEBUG?.info('Command changed after settle (re-validate)', { messageId });
|
RC_DEBUG?.info('Command changed after settle (re-validate)', { messageId });
|
||||||
message.originalText = stable;
|
message.originalText = stable;
|
||||||
const reParsed = CommandParser.parseYAMLCommand(stable);
|
const reParsed = CommandParser.parseYAMLCommand(stable);
|
||||||
const reVal = CommandParser.validateStructure(reParsed);
|
const reVal = CommandParser.validateStructure(reParsed);
|
||||||
if (!reVal.isValid) throw new Error(`Final validation failed: ${reVal.errors.join(', ')}`);
|
if (!reVal.isValid) {
|
||||||
|
this.attachRetryUI(message.element, messageId);
|
||||||
|
throw new Error(`Final validation failed: ${reVal.errors.join(', ')}`);
|
||||||
|
}
|
||||||
parsed = reParsed;
|
parsed = reParsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) Pre-mark to avoid duplicate runs even if failure later (explicit design)
|
// 4) Execute
|
||||||
this.history.mark(message.originalText);
|
|
||||||
|
|
||||||
// 5) Execute
|
|
||||||
this.updateState(messageId, COMMAND_STATES.EXECUTING);
|
this.updateState(messageId, COMMAND_STATES.EXECUTING);
|
||||||
const result = await ExecutionManager.executeCommand(parsed, message.element);
|
const result = await ExecutionManager.executeCommand(parsed, message.element);
|
||||||
|
|
||||||
if (!result || result.success === false) {
|
if (!result || result.success === false) {
|
||||||
RC_DEBUG?.warn('Execution reported failure; command remains marked (no auto-retry)', { messageId });
|
RC_DEBUG?.warn('Execution reported failure', { messageId });
|
||||||
this.updateState(messageId, COMMAND_STATES.ERROR);
|
this.updateState(messageId, COMMAND_STATES.ERROR);
|
||||||
|
this.attachRetryUI(message.element, messageId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const duration = Date.now() - started;
|
const duration = Date.now() - started;
|
||||||
if (duration < 50) RC_DEBUG?.warn('Command completed very fast', { messageId, duration });
|
if (duration < CONFIG.FAST_WARN_MS) RC_DEBUG?.warn('Command completed very fast', { messageId, duration });
|
||||||
if (duration > 60000) RC_DEBUG?.warn('Command took very long', { messageId, duration });
|
if (duration > CONFIG.SLOW_WARN_MS) RC_DEBUG?.warn('Command took very long', { messageId, duration });
|
||||||
|
|
||||||
this.updateState(messageId, COMMAND_STATES.COMPLETE);
|
this.updateState(messageId, COMMAND_STATES.COMPLETE);
|
||||||
|
|
||||||
|
|
@ -1219,26 +1583,36 @@
|
||||||
RC_DEBUG?.error(`Command processing error: ${error.message}`, { messageId, duration });
|
RC_DEBUG?.error(`Command processing error: ${error.message}`, { messageId, duration });
|
||||||
this.updateState(messageId, COMMAND_STATES.ERROR);
|
this.updateState(messageId, COMMAND_STATES.ERROR);
|
||||||
const message = this.trackedMessages.get(messageId);
|
const message = this.trackedMessages.get(messageId);
|
||||||
if (/No complete \^%\$bridge/.test(error.message)) return; // quiet for partials
|
if (/No complete \^%\$bridge/.test(error.message)) return;
|
||||||
if (message) {
|
if (message) {
|
||||||
|
this.attachRetryUI(message.element, messageId);
|
||||||
UIFeedback.appendStatus(message.element, 'ERROR', { action: 'Command', details: error.message });
|
UIFeedback.appendStatus(message.element, 'ERROR', { action: 'Command', details: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debounce() { return new Promise((r) => setTimeout(r, CONFIG.DEBOUNCE_DELAY)); }
|
|
||||||
|
|
||||||
cleanupProcessedCommands() {
|
cleanupProcessedCommands() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
for (const [id, msg] of this.trackedMessages.entries()) {
|
for (const [id, msg] of this.trackedMessages.entries()) {
|
||||||
if ((msg.state === COMMAND_STATES.COMPLETE || msg.state === COMMAND_STATES.ERROR) &&
|
const age = now - (msg.lastUpdate || msg.startTime || now);
|
||||||
now - (msg.lastUpdate || now) > CONFIG.CLEANUP_AFTER_MS) {
|
const finished = (msg.state === COMMAND_STATES.COMPLETE || msg.state === COMMAND_STATES.ERROR);
|
||||||
|
|
||||||
|
const shouldCleanup =
|
||||||
|
(finished && age > CONFIG.CLEANUP_AFTER_MS) ||
|
||||||
|
(age > CONFIG.STUCK_AFTER_MS); // Force cleanup for stuck items
|
||||||
|
|
||||||
|
if (shouldCleanup) {
|
||||||
|
if (age > CONFIG.STUCK_AFTER_MS && !finished) {
|
||||||
|
RC_DEBUG?.warn('Cleaning stuck entry', { messageId: id, state: msg.state, age });
|
||||||
|
}
|
||||||
this.trackedMessages.delete(id);
|
this.trackedMessages.delete(id);
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (count) RC_DEBUG?.info(`Cleaned ${count} processed entries`);
|
|
||||||
|
if (count) RC_DEBUG?.info(`Cleaned ${count} tracked entrie(s)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
stopAllProcessing() {
|
stopAllProcessing() {
|
||||||
|
|
@ -1257,12 +1631,16 @@
|
||||||
CONFIG.RUNTIME.PAUSED = true;
|
CONFIG.RUNTIME.PAUSED = true;
|
||||||
saveConfig(CONFIG);
|
saveConfig(CONFIG);
|
||||||
|
|
||||||
|
// Cancel all pending operations
|
||||||
for (const [id, msg] of this.trackedMessages.entries()) {
|
for (const [id, msg] of this.trackedMessages.entries()) {
|
||||||
|
if (msg.cancelToken) msg.cancelToken.cancelled = true;
|
||||||
|
|
||||||
if (msg.state === COMMAND_STATES.EXECUTING || msg.state === COMMAND_STATES.DEBOUNCING) {
|
if (msg.state === COMMAND_STATES.EXECUTING || msg.state === COMMAND_STATES.DEBOUNCING) {
|
||||||
RC_DEBUG?.error('Emergency stop - cancelling command', { messageId: id });
|
RC_DEBUG?.error('Emergency stop - cancelling command', { messageId: id });
|
||||||
this.updateState(id, COMMAND_STATES.ERROR);
|
this.updateState(id, COMMAND_STATES.ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.stopAllProcessing();
|
this.stopAllProcessing();
|
||||||
RC_DEBUG?.error('🚨 EMERGENCY STOP ACTIVATED 🚨');
|
RC_DEBUG?.error('🚨 EMERGENCY STOP ACTIVATED 🚨');
|
||||||
GM_notification({ text: 'All command processing stopped', title: 'Emergency Stop', timeout: 5000 });
|
GM_notification({ text: 'All command processing stopped', title: 'Emergency Stop', timeout: 5000 });
|
||||||
|
|
@ -1273,20 +1651,20 @@
|
||||||
// ---------------------- Manual retry helpers ----------------------
|
// ---------------------- Manual retry helpers ----------------------
|
||||||
let commandMonitor; // forward ref
|
let commandMonitor; // forward ref
|
||||||
|
|
||||||
window.AI_REPO_RETRY_COMMAND_TEXT = (text) => {
|
window.AI_REPO_RETRY_COMMAND_TEXT = () => {
|
||||||
try {
|
RC_DEBUG?.warn('Retry by text is deprecated. Use AI_REPO_RETRY_MESSAGE(messageId) or click "Run again".');
|
||||||
commandMonitor?.history?.unmark?.(text);
|
|
||||||
RC_DEBUG?.info('Command unmarked for manual retry (by text)', { preview: String(text).slice(0,120) });
|
|
||||||
} catch (e) {
|
|
||||||
RC_DEBUG?.error('Failed to unmark command by text', { error: String(e) });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.AI_REPO_RETRY_MESSAGE = (messageId) => {
|
window.AI_REPO_RETRY_MESSAGE = (messageId) => {
|
||||||
try {
|
try {
|
||||||
const msg = commandMonitor?.trackedMessages?.get(messageId);
|
const msg = commandMonitor?.trackedMessages?.get(messageId);
|
||||||
if (!msg) { RC_DEBUG?.warn('Message not found for retry', { messageId }); return; }
|
if (!msg) {
|
||||||
commandMonitor.history.unmark(msg.originalText);
|
RC_DEBUG?.warn('Message not found for retry', { messageId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.element.dataset.aiRcProcessed = '1'; // ← Block scan loop
|
||||||
|
|
||||||
RC_DEBUG?.info('Message unmarked; reprocessing now', { messageId });
|
RC_DEBUG?.info('Message unmarked; reprocessing now', { messageId });
|
||||||
commandMonitor.updateState(messageId, COMMAND_STATES.PARSING);
|
commandMonitor.updateState(messageId, COMMAND_STATES.PARSING);
|
||||||
commandMonitor.processCommand(messageId);
|
commandMonitor.processCommand(messageId);
|
||||||
|
|
@ -1295,6 +1673,7 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// ---------------------- Test commands ----------------------
|
// ---------------------- Test commands ----------------------
|
||||||
const TEST_COMMANDS = {
|
const TEST_COMMANDS = {
|
||||||
validUpdate:
|
validUpdate:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue