Update src/ai-repo-commander.user.js

No more premature detection - Commands won't be processed until the full --- terminator appears

No more history pollution - Incomplete commands won't get marked as "already executed"

Better streaming resilience - Waits for the command to stabilize before processing

Configurable timing - You can adjust settle times in the Tools panel
This commit is contained in:
rob 2025-10-07 21:30:30 +00:00
parent 5acd23f595
commit 59379336e2
1 changed files with 360 additions and 163 deletions

View File

@ -1,8 +1,8 @@
// ==UserScript== // ==UserScript==
// @name AI Repo Commander // @name AI Repo Commander
// @namespace http://tampermonkey.net/ // @namespace http://tampermonkey.net/
// @version 1.3.4 // @version 1.4.0
// @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 (safe manual-retry only) // @description Execute ^%$bridge YAML commands from AI assistants (safe & robust): complete-block detection, streaming-settle, persistent dedupe, paste+autosubmit, debug console with Tools/Settings, draggable/collapsible panel
// @author Your Name // @author Your Name
// @match https://chat.openai.com/* // @match https://chat.openai.com/*
// @match https://chatgpt.com/* // @match https://chatgpt.com/*
@ -17,40 +17,68 @@
(function () { (function () {
'use strict'; 'use strict';
// ---------------------- Config ---------------------- // ---------------------- Storage keys ----------------------
const CONFIG = { const STORAGE_KEYS = {
ENABLE_API: true, // Master kill switch (STOP API flips this to false) history: 'ai_repo_commander_executed',
DEBUG_MODE: true, // Global on/off for debug logging cfg: 'ai_repo_commander_cfg',
DEBUG_LEVEL: 2, // 0=off, 1=errors, 2=info, 3=verbose, 4=trace panel: 'ai_repo_commander_panel_state'
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.4',
PROCESS_EXISTING: false, // If false, only process *new* messages (no initial rescan) // ---------------------- Config (with persistence) ----------------------
ASSISTANT_ONLY: true, // Process assistant messages by default (core use case) const DEFAULT_CONFIG = {
ENABLE_API: true,
DEBUG_MODE: true,
DEBUG_LEVEL: 2, // 0=off, 1=errors, 2=info, 3=verbose, 4=trace
DEBUG_WATCH_MS: 120000,
DEBUG_MAX_LINES: 400,
DEBUG_SHOW_PANEL: true,
DEBOUNCE_DELAY: 5000, // bot-typing protection
MAX_RETRIES: 2,
VERSION: '1.4.0',
PROCESS_EXISTING: false,
ASSISTANT_ONLY: true,
// 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
// Housekeeping // Housekeeping
CLEANUP_AFTER_MS: 30000, // Drop COMPLETE/ERROR entries after 30s CLEANUP_AFTER_MS: 30000,
CLEANUP_INTERVAL_MS: 60000, // Sweep cadence CLEANUP_INTERVAL_MS: 60000,
// Paste + submit behavior // Paste + submit behavior
APPEND_TRAILING_NEWLINE: true, // Add '\n' after pasted text APPEND_TRAILING_NEWLINE: true,
AUTO_SUBMIT: true, // Try to submit after pasting content AUTO_SUBMIT: true,
POST_PASTE_DELAY_MS: 250, // Delay before submit to let editors settle POST_PASTE_DELAY_MS: 250,
SUBMIT_MODE: 'button_first', // 'button_first' | 'enter_only' | 'smart' SUBMIT_MODE: 'button_first', // 'button_first' | 'enter_only' | 'smart'
// Runtime toggles (live-updated by the debug panel) // Streaming-complete hardening (DeepSeek #2)
RUNTIME: { REQUIRE_TERMINATOR: true, // require trailing '---' line
PAUSED: false, // Pause scanning + execution via panel SETTLE_CHECK_MS: 1500, // time to see the block remain unchanged
} SETTLE_POLL_MS: 300, // poll interval for stability
// Runtime toggles
RUNTIME: { PAUSED: false }
}; };
function loadSavedConfig() {
try {
const raw = localStorage.getItem(STORAGE_KEYS.cfg);
if (!raw) return structuredClone(DEFAULT_CONFIG);
const saved = JSON.parse(raw);
// shallow merge; keep nested RUNTIME
const merged = { ...DEFAULT_CONFIG, ...saved, RUNTIME: { ...DEFAULT_CONFIG.RUNTIME, ...(saved.RUNTIME || {}) } };
return merged;
} catch {
return structuredClone(DEFAULT_CONFIG);
}
}
function saveConfig(cfg) {
try { localStorage.setItem(STORAGE_KEYS.cfg, JSON.stringify(cfg)); } catch {}
}
const CONFIG = loadSavedConfig();
// ---------------------- Debug Console ---------------------- // ---------------------- Debug Console ----------------------
let RC_DEBUG = null; let RC_DEBUG = null;
@ -61,8 +89,12 @@
this.loopCounts = new Map(); this.loopCounts = new Map();
this.startedAt = Date.now(); this.startedAt = Date.now();
this.panel = null; this.panel = null;
this.bodyLogs = null;
this.bodyTools = null;
this.collapsed = false;
this.drag = { active: false, dx: 0, dy: 0 };
this.panelState = this._loadPanelState();
// loop-counts periodic cleanup to avoid unbounded growth
this.loopCleanupInterval = setInterval(() => { this.loopCleanupInterval = setInterval(() => {
if (Date.now() - this.startedAt > this.cfg.DEBUG_WATCH_MS * 2) { if (Date.now() - this.startedAt > this.cfg.DEBUG_WATCH_MS * 2) {
this.loopCounts.clear(); this.loopCounts.clear();
@ -74,7 +106,21 @@
if (cfg.DEBUG_SHOW_PANEL) this.mount(); if (cfg.DEBUG_SHOW_PANEL) this.mount();
this.info(`Debug console ready (level=${cfg.DEBUG_LEVEL})`); this.info(`Debug console ready (level=${cfg.DEBUG_LEVEL})`);
} }
// Levels: 1=ERROR, 2=WARN, 3=INFO, 4=VERB, 5=TRACE
_loadPanelState() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEYS.panel) || '{}');
} catch { return {}; }
}
_savePanelState(partial) {
try {
const merged = { ...(this.panelState || {}), ...(partial || {}) };
this.panelState = merged;
localStorage.setItem(STORAGE_KEYS.panel, JSON.stringify(merged));
} catch {}
}
// Levels
error(msg, data) { this._log(1, 'ERROR', msg, data); } error(msg, data) { this._log(1, 'ERROR', msg, data); }
warn(msg, data) { this._log(2, 'WARN', msg, data); } warn(msg, data) { this._log(2, 'WARN', msg, data); }
info(msg, data) { this._log(3, 'INFO', msg, data); } info(msg, data) { this._log(3, 'INFO', msg, data); }
@ -89,7 +135,6 @@
nowIso() { return new Date().toISOString(); } nowIso() { return new Date().toISOString(); }
withinWatch() { return Date.now() - this.startedAt <= this.cfg.DEBUG_WATCH_MS; } 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) { logLoop(kind, msg) {
const k = `${kind}:${msg}`; const k = `${kind}:${msg}`;
const cur = this.loopCounts.get(k) || 0; const cur = this.loopCounts.get(k) || 0;
@ -97,7 +142,6 @@
if (cur >= 10) return; if (cur >= 10) return;
this.loopCounts.set(k, cur + 1); this.loopCounts.set(k, cur + 1);
const suffix = (cur + 1) > 1 ? ` (${cur + 1}x)` : ''; const suffix = (cur + 1) > 1 ? ` (${cur + 1}x)` : '';
// default to INFO (visible at level 2+)
if (kind === 'ERROR') this.error(`${msg}${suffix}`); if (kind === 'ERROR') this.error(`${msg}${suffix}`);
else if (kind === 'WARN') this.warn(`${msg}${suffix}`); else if (kind === 'WARN') this.warn(`${msg}${suffix}`);
else this.info(`${msg}${suffix}`); else this.info(`${msg}${suffix}`);
@ -134,8 +178,9 @@
} }
setLevel(n) { setLevel(n) {
const lv = Math.max(0, Math.min(4, n)); // clamp 0..4 const lv = Math.max(0, Math.min(4, n));
this.cfg.DEBUG_LEVEL = lv; this.cfg.DEBUG_LEVEL = lv;
saveConfig(this.cfg);
this.info(`Log level => ${lv}`); this.info(`Log level => ${lv}`);
} }
@ -155,8 +200,6 @@
_log(numericLevel, levelName, msg, data) { _log(numericLevel, levelName, msg, data) {
if (!this.cfg.DEBUG_MODE) return; 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 thresholdMap = { 0: 0, 1: 1, 2: 3, 3: 4, 4: 5 };
const threshold = thresholdMap[this.cfg.DEBUG_LEVEL] ?? 0; const threshold = thresholdMap[this.cfg.DEBUG_LEVEL] ?? 0;
if (numericLevel > threshold) return; if (numericLevel > threshold) return;
@ -164,34 +207,34 @@
const entry = { ts: this.nowIso(), level: levelName, msg: String(msg), data: this._sanitize(data) }; const entry = { ts: this.nowIso(), level: levelName, msg: String(msg), data: this._sanitize(data) };
this.buf.push(entry); 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); 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) { if (this.cfg.DEBUG_LEVEL >= 3) {
const prefix = `[AI RC]`; const prefix = `[AI RC]`;
if (entry.data != null) console.log(prefix, entry.level, entry.msg, entry.data); if (entry.data != null) console.log(prefix, entry.level, entry.msg, entry.data);
else console.log(prefix, entry.level, entry.msg); else console.log(prefix, entry.level, entry.msg);
} }
if (this.panel) this._renderRow(entry); if (this.panel) this._renderRow(entry);
} }
mount() { mount() {
if (!document.body) { if (!document.body) { setTimeout(() => this.mount(), 100); return; }
setTimeout(() => this.mount(), 100);
return;
}
const root = document.createElement('div'); const root = document.createElement('div');
root.style.cssText = ` root.style.cssText = `
position: fixed; right: 16px; bottom: 16px; z-index: 2147483647; position: fixed; ${this.panelState.left!==undefined ? `left:${this.panelState.left}px; top:${this.panelState.top}px;` : 'right:16px; bottom:16px;'}
width: 420px; max-height: 45vh; display: flex; flex-direction: column; 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; 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; 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); box-shadow: 0 16px 40px rgba(0,0,0,0.55); backdrop-filter: blur(4px);
`; `;
root.innerHTML = ` root.innerHTML = `
<div style="display:flex; gap:8px; align-items:center; padding:8px; border-bottom:1px solid #2c2c33"> <div class="rc-header" style="display:flex; gap:8px; align-items:center; padding:8px; border-bottom:1px solid #2c2c33; cursor:move; user-select:none">
<strong style="flex:1">AI Repo Commander Debug</strong> <strong style="flex:1">AI Repo Commander</strong>
<label style="display:flex;align-items:center;gap:4px;">Level <div class="rc-tabs" style="display:flex; gap:6px;">
<button class="rc-tab rc-tab-logs" style="padding:4px 6px;border:1px solid #374151;border-radius:4px;background:#1f2937;color:#e5e7eb;">Logs</button>
<button class="rc-tab rc-tab-tools" style="padding:4px 6px;border:1px solid #374151;border-radius:4px;background:#111827;color:#e5e7eb;">Tools & Settings</button>
</div>
<label style="display:flex;align-items:center;gap:4px;">Lvl
<select class="rc-level" style="background:#111827;color:#e5e7eb;border:1px solid #374151;border-radius:4px;padding:2px 6px;"> <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="0">off</option>
<option value="1">errors</option> <option value="1">errors</option>
@ -200,24 +243,77 @@
<option value="4">trace</option> <option value="4">trace</option>
</select> </select>
</label> </label>
<button class="rc-copy" title="Copy last 50 lines" style="padding:4px 6px;">Copy 50</button> <button class="rc-copy" title="Copy last 50 lines" style="padding:4px 6px;">Copy</button>
<button class="rc-pause" title="Pause/resume scanning" style="padding:4px 6px;">Pause</button> <button class="rc-pause" title="Pause/resume scanning" style="padding:4px 6px;">Pause</button>
<button class="rc-collapse" title="Collapse/expand" style="padding:4px 6px;"></button>
<button class="rc-stop" title="Stop API calls" style="padding:4px 6px;background:#7f1d1d;color:#fff;border:1px solid #991b1b">STOP API</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>
<div class="rc-body" style="overflow:auto; padding:8px; display:block; flex:1"></div> <div class="rc-body rc-body-logs" style="overflow:auto; padding:8px; display:block; flex:1"></div>
<div class="rc-body rc-body-tools" style="overflow:auto; padding:8px; display:none; flex:1">
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
<div style="grid-column:1 / -1;">
<h4 style="margin:0 0 6px 0;">Quick Actions</h4>
<button class="rc-clear-history" style="padding:6px 8px;border:1px solid #374151;border-radius:6px;background:#1f2937;color:#e5e7eb;">Clear History</button>
<button class="rc-copy-200" style="padding:6px 8px;border:1px solid #374151;border-radius:6px;background:#1f2937;color:#e5e7eb;margin-left:8px;">Copy last 200 logs</button>
</div>
<div>
<h4 style="margin:8px 0 6px 0;">Toggles</h4>
<label style="display:flex;align-items:center;gap:8px;margin:4px 0;">
<input class="rc-toggle" data-key="ENABLE_API" type="checkbox"> ENABLE_API
</label>
<label style="display:flex;align-items:center;gap:8px;margin:4px 0;">
<input class="rc-toggle" data-key="AUTO_SUBMIT" type="checkbox"> AUTO_SUBMIT
</label>
<label style="display:flex;align-items:center;gap:8px;margin:4px 0;">
<input class="rc-toggle" data-key="APPEND_TRAILING_NEWLINE" type="checkbox"> APPEND_NEWLINE
</label>
<label style="display:flex;align-items:center;gap:8px;margin:4px 0;">
<input class="rc-toggle" data-key="ASSISTANT_ONLY" type="checkbox"> ASSISTANT_ONLY
</label>
<label style="display:flex;align-items:center;gap:8px;margin:4px 0;">
<input class="rc-toggle" data-key="PROCESS_EXISTING" type="checkbox"> PROCESS_EXISTING
</label>
</div>
<div>
<h4 style="margin:8px 0 6px 0;">Delays</h4>
<label style="display:flex;align-items:center;gap:8px;margin:4px 0;">
DEBOUNCE_DELAY (ms) <input class="rc-num" data-key="DEBOUNCE_DELAY" type="number" min="0" step="100" style="width:120px;background:#0b1220;color:#e5e7eb;border:1px solid #374151;border-radius:4px;padding:2px 6px;">
</label>
<label style="display:flex;align-items:center;gap:8px;margin:4px 0;">
SETTLE_CHECK_MS <input class="rc-num" data-key="SETTLE_CHECK_MS" type="number" min="0" step="100" style="width:120px;background:#0b1220;color:#e5e7eb;border:1px solid #374151;border-radius:4px;padding:2px 6px;">
</label>
<label style="display:flex;align-items:center;gap:8px;margin:4px 0;">
SETTLE_POLL_MS <input class="rc-num" data-key="SETTLE_POLL_MS" type="number" min="50" step="50" style="width:120px;background:#0b1220;color:#e5e7eb;border:1px solid #374151;border-radius:4px;padding:2px 6px;">
</label>
</div>
<div style="grid-column:1 / -1;">
<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>
<div style="margin-top:6px;">
<button class="rc-save-json" style="padding:6px 8px;border:1px solid #374151;border-radius:6px;background:#1f2937;color:#e5e7eb;">Save Config</button>
<button class="rc-reset-defaults" style="padding:6px 8px;border:1px solid #374151;border-radius:6px;background:#1f2937;color:#e5e7eb;margin-left:8px;">Reset to Defaults</button>
</div>
</div>
</div>
</div>
`; `;
document.body.appendChild(root); document.body.appendChild(root);
this.panel = root; this.panel = root;
this.bodyLogs = root.querySelector('.rc-body-logs');
this.bodyTools = root.querySelector('.rc-body-tools');
// Controls
const sel = root.querySelector('.rc-level'); const sel = root.querySelector('.rc-level');
sel.value = String(this.cfg.DEBUG_LEVEL); sel.value = String(this.cfg.DEBUG_LEVEL);
sel.addEventListener('change', () => this.setLevel(parseInt(sel.value,10))); sel.addEventListener('change', () => this.setLevel(parseInt(sel.value,10)));
root.querySelector('.rc-copy').addEventListener('click', () => this.copyLast(50)); root.querySelector('.rc-copy').addEventListener('click', () => this.copyLast(50));
root.querySelector('.rc-copy-200').addEventListener('click', () => this.copyLast(200));
const pauseBtn = root.querySelector('.rc-pause'); const pauseBtn = root.querySelector('.rc-pause');
pauseBtn.addEventListener('click', () => { pauseBtn.addEventListener('click', () => {
this.cfg.RUNTIME.PAUSED = !this.cfg.RUNTIME.PAUSED; this.cfg.RUNTIME.PAUSED = !this.cfg.RUNTIME.PAUSED;
saveConfig(this.cfg);
pauseBtn.textContent = this.cfg.RUNTIME.PAUSED ? 'Resume' : 'Pause'; pauseBtn.textContent = this.cfg.RUNTIME.PAUSED ? 'Resume' : 'Pause';
pauseBtn.style.background = this.cfg.RUNTIME.PAUSED ? '#f59e0b' : ''; pauseBtn.style.background = this.cfg.RUNTIME.PAUSED ? '#f59e0b' : '';
pauseBtn.style.color = this.cfg.RUNTIME.PAUSED ? '#111827' : ''; pauseBtn.style.color = this.cfg.RUNTIME.PAUSED ? '#111827' : '';
@ -228,16 +324,133 @@
window.AI_REPO_STOP?.(); window.AI_REPO_STOP?.();
this.warn('Emergency STOP activated'); this.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);
// refresh tools view values
root.querySelectorAll('.rc-toggle').forEach(inp => {
const key = inp.dataset.key;
inp.checked = !!this.cfg[key];
});
root.querySelectorAll('.rc-num').forEach(inp => {
inp.value = String(this.cfg[inp.dataset.key] ?? '');
});
root.querySelector('.rc-json').value = JSON.stringify(this.cfg, null, 2);
});
// 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) => {
if ((e.target).closest('button,select,input,textarea,label')) return; // let controls work
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);
};
// Tools: Clear History
root.querySelector('.rc-clear-history').addEventListener('click', () => {
localStorage.removeItem(STORAGE_KEYS.history);
this.info('Command history cleared');
GM_notification({ title: 'AI Repo Commander', text: 'Command history cleared', timeout: 2500 });
});
// Tools: toggles & numbers
root.querySelectorAll('.rc-toggle').forEach(inp => {
const key = inp.dataset.key;
inp.checked = !!this.cfg[key];
inp.addEventListener('change', () => {
this.cfg[key] = !!inp.checked;
saveConfig(this.cfg);
this.info(`Config ${key} => ${this.cfg[key]}`);
});
});
root.querySelectorAll('.rc-num').forEach(inp => {
inp.value = String(this.cfg[inp.dataset.key] ?? '');
inp.addEventListener('change', () => {
const v = parseInt(inp.value, 10);
if (!Number.isNaN(v)) {
this.cfg[inp.dataset.key] = v;
saveConfig(this.cfg);
this.info(`Config ${inp.dataset.key} => ${v}`);
}
});
});
// Tools: JSON input
root.querySelector('.rc-save-json').addEventListener('click', () => {
try {
const raw = root.querySelector('.rc-json').value;
const parsed = JSON.parse(raw);
Object.assign(this.cfg, parsed);
saveConfig(this.cfg);
this.info('Config JSON saved');
} catch (e) {
this.warn('Invalid JSON in config textarea', { error: String(e) });
}
});
root.querySelector('.rc-reset-defaults').addEventListener('click', () => {
Object.assign(this.cfg, structuredClone(DEFAULT_CONFIG));
saveConfig(this.cfg);
this.info('Config reset to defaults');
});
// Set initial UI states
const pauseBtn = root.querySelector('.rc-pause');
pauseBtn.textContent = this.cfg.RUNTIME.PAUSED ? 'Resume' : 'Pause';
if (this.cfg.RUNTIME.PAUSED) {
pauseBtn.style.background = '#f59e0b';
pauseBtn.style.color = '#111827';
}
} }
_renderRow(e) { _renderRow(e) {
const body = this.panel.querySelector('.rc-body'); if (!this.bodyLogs) return;
const row = document.createElement('div'); 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.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): ''}`; row.textContent = `${e.ts} ${e.level.padEnd(5)} ${e.msg}${e.data? ' ' + JSON.stringify(e.data): ''}`;
body.appendChild(row); this.bodyLogs.appendChild(row);
while (body.children.length > this.cfg.DEBUG_MAX_LINES) body.firstChild.remove(); while (this.bodyLogs.children.length > this.cfg.DEBUG_MAX_LINES) this.bodyLogs.firstChild.remove();
body.scrollTop = body.scrollHeight; this.bodyLogs.scrollTop = this.bodyLogs.scrollHeight;
} }
destroy() { destroy() {
@ -248,10 +461,10 @@
// ---------------------- Platform selectors ---------------------- // ---------------------- Platform selectors ----------------------
const PLATFORM_SELECTORS = { const PLATFORM_SELECTORS = {
'chat.openai.com': { messages: '[data-message-author-role]', input: '#prompt-textarea, textarea, [contenteditable="true"]', content: '.markdown' }, 'chat.openai.com': { messages: '[data-message-author-role]', input: '#prompt-textarea, textarea, [contenteditable="true"]', content: '.markdown' },
'chatgpt.com': { messages: '[data-message-author-role]', input: '#prompt-textarea, textarea, [contenteditable="true"]', content: '.markdown' }, 'chatgpt.com': { messages: '[data-message-author-role]', input: '#prompt-textarea, textarea, [contenteditable="true"]', content: '.markdown' },
'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' }
}; };
// ---------------------- Command requirements ---------------------- // ---------------------- Command requirements ----------------------
@ -293,7 +506,7 @@
// ---------------------- Persistent Command History ---------------------- // ---------------------- Persistent Command History ----------------------
class CommandHistory { class CommandHistory {
constructor() { constructor() {
this.key = 'ai_repo_commander_executed'; this.key = STORAGE_KEYS.history;
this.ttl = CONFIG.DEDUPE_TTL_MS; this.ttl = CONFIG.DEDUPE_TTL_MS;
this.cleanup(); this.cleanup();
} }
@ -318,7 +531,7 @@
db[this._hash(text)] = Date.now(); db[this._hash(text)] = Date.now();
this._save(db); this._save(db);
} }
unmark(text) { // manual retry only unmark(text) {
const db = this._load(); const db = this._load();
const k = this._hash(text); const k = this._hash(text);
if (k in db) { delete db[k]; this._save(db); } if (k in db) { delete db[k]; this._save(db); }
@ -334,7 +547,13 @@
} }
reset() { localStorage.removeItem(this.key); } reset() { localStorage.removeItem(this.key); }
} }
window.AI_REPO_CLEAR_HISTORY = () => localStorage.removeItem('ai_repo_commander_executed');
// Global helpers (stable)
window.AI_REPO = {
clearHistory: () => localStorage.removeItem(STORAGE_KEYS.history),
getConfig: () => structuredClone(CONFIG),
setConfig: (patch) => { Object.assign(CONFIG, patch||{}); saveConfig(CONFIG); },
};
// ---------------------- UI feedback ---------------------- // ---------------------- UI feedback ----------------------
class UIFeedback { class UIFeedback {
@ -410,12 +629,7 @@
const events = ['keydown','keypress','keyup']; const events = ['keydown','keypress','keyup'];
for (const type of events) { for (const type of events) {
const ok = el.dispatchEvent(new KeyboardEvent(type, { const ok = el.dispatchEvent(new KeyboardEvent(type, {
key: 'Enter', key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true
code: 'Enter',
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true
})); }));
if (!ok) return false; if (!ok) return false;
} }
@ -425,10 +639,7 @@
async function submitComposer() { async function submitComposer() {
try { try {
const btn = findSendButton(); const btn = findSendButton();
if (CONFIG.SUBMIT_MODE !== 'enter_only' && btn) { if (CONFIG.SUBMIT_MODE !== 'enter_only' && btn) { btn.click(); return true; }
btn.click();
return true;
}
const el = getVisibleInputCandidate(); const el = getVisibleInputCandidate();
if (!el) return false; if (!el) return false;
return pressEnterOn(el); return pressEnterOn(el);
@ -444,33 +655,25 @@
GM_notification({ title: 'AI Repo Commander', text: 'No input box found to paste file content.', timeout: 4000 }); GM_notification({ title: 'AI Repo Commander', text: 'No input box found to paste file content.', timeout: 4000 });
return false; return false;
} }
const payload = CONFIG.APPEND_TRAILING_NEWLINE ? (text.endsWith('\n') ? text : text + '\n') : text; const payload = CONFIG.APPEND_TRAILING_NEWLINE ? (text.endsWith('\n') ? text : text + '\n') : text;
el.focus(); el.focus();
// 1) ClipboardEvent paste
try { try {
const dt = new DataTransfer(); const dt = new DataTransfer();
dt.setData('text/plain', payload); dt.setData('text/plain', payload);
const pasteEvt = new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true }); const pasteEvt = new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true });
if (el.dispatchEvent(pasteEvt) && !pasteEvt.defaultPrevented) return true; if (el.dispatchEvent(pasteEvt) && !pasteEvt.defaultPrevented) return true;
} catch (_) { /* continue */ } } catch (_) {}
// 2) execCommand insertText
try { try {
const sel = window.getSelection && window.getSelection(); const sel = window.getSelection && window.getSelection();
if (sel && sel.rangeCount === 0 && el instanceof HTMLElement) { if (sel && sel.rangeCount === 0 && el instanceof HTMLElement) {
const r = document.createRange(); const r = document.createRange();
r.selectNodeContents(el); r.selectNodeContents(el); r.collapse(false); sel.removeAllRanges(); sel.addRange(r);
r.collapse(false);
sel.removeAllRanges();
sel.addRange(r);
} }
if (document.execCommand && document.execCommand('insertText', false, payload)) return true; if (document.execCommand && document.execCommand('insertText', false, payload)) return true;
} catch (_) { /* continue */ } } catch (_) {}
// 3) ProseMirror innerHTML
const isPM = el.classList && el.classList.contains('ProseMirror'); const isPM = el.classList && el.classList.contains('ProseMirror');
if (isPM) { if (isPM) {
const escape = (s) => s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); const escape = (s) => s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
@ -481,19 +684,13 @@
return true; return true;
} }
// 4) contenteditable/textarea fallback
if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') { if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
el.value = payload; el.value = payload; el.dispatchEvent(new Event('input', { bubbles: true })); return true;
el.dispatchEvent(new Event('input', { bubbles: true }));
return true;
} }
if (el.isContentEditable || el.getAttribute('contenteditable') === 'true') { if (el.isContentEditable || el.getAttribute('contenteditable') === 'true') {
el.textContent = payload; el.textContent = payload; el.dispatchEvent(new Event('input', { bubbles: true })); return true;
el.dispatchEvent(new Event('input', { bubbles: true }));
return true;
} }
// 5) Clipboard fallback
try { try {
if (typeof GM_setClipboard === 'function') { if (typeof GM_setClipboard === 'function') {
GM_setClipboard(payload, { type: 'text', mimetype: 'text/plain' }); GM_setClipboard(payload, { type: 'text', mimetype: 'text/plain' });
@ -520,11 +717,11 @@
return true; return true;
} }
// ---------------------- Parser ---------------------- // ---------------------- Parser (strict, require ---) ----------------------
class CommandParser { class CommandParser {
static parseYAMLCommand(codeBlockText) { static parseYAMLCommand(codeBlockText) {
const block = this.extractCommandBlock(codeBlockText); const block = this.extractCompleteBlock(codeBlockText);
if (!block) throw new Error('No valid command block found'); if (!block) throw new Error('No complete ^%$bridge command found (missing --- terminator).');
const parsed = this.parseKeyValuePairs(block); const parsed = this.parseKeyValuePairs(block);
// defaults // defaults
@ -540,17 +737,14 @@
return parsed; return parsed;
} }
static extractCommandBlock(text) { static extractCompleteBlock(text) {
const patterns = [ // Require terminator line --- (DeepSeek #2)
/^\s*\^%\$bridge[ \t]*\n([\s\S]*?)\n---[ \t]*(?:\n|$)/m, const pattern = /^\s*\^%\$bridge[ \t]*\n([\s\S]*?)\n---[ \t]*(?:\n|$)/m;
/^\s*\^%\$bridge[ \t]*\n([\s\S]*?)(?=\n\s*$|\n---|\n```|$)/m, const m = text.match(pattern);
/^\s*\^%\$bridge[ \t]*\n([\s\S]*)/m if (!m) return null;
]; const inner = m[1]?.trimEnd();
for (const pattern of patterns) { if (!inner) return null;
const match = text.match(pattern); return inner;
if (match && match[1]?.trim()) return match[1].trimEnd();
}
return null;
} }
static parseKeyValuePairs(block) { static parseKeyValuePairs(block) {
@ -580,7 +774,6 @@
if (idx !== -1) { if (idx !== -1) {
const key = line.slice(0, idx).trim(); const key = line.slice(0, idx).trim();
let value = line.slice(idx + 1).trim(); let value = line.slice(idx + 1).trim();
if (value === '|') { if (value === '|') {
currentKey = key; collecting = true; buf = []; currentKey = key; collecting = true; buf = [];
} else if (value === '') { } else if (value === '') {
@ -664,7 +857,7 @@
} }
static async mockExecution(command, sourceElement) { static async mockExecution(command, sourceElement) {
await this.delay(1000); await this.delay(500);
const mock = { const mock = {
status: 200, status: 200,
responseText: JSON.stringify({ responseText: JSON.stringify({
@ -770,10 +963,10 @@
return BRIDGE_KEY; return BRIDGE_KEY;
} }
// ---------------------- Monitor ---------------------- // ---------------------- Monitor (with streaming “settle” & complete-block check) ----------------------
class CommandMonitor { class CommandMonitor {
constructor() { constructor() {
this.trackedMessages = new Map(); // id -> { element, originalText, state, lastUpdate, startTime } this.trackedMessages = new Map();
this.history = new CommandHistory(); this.history = new CommandHistory();
this.observer = null; this.observer = null;
this.currentPlatform = null; this.currentPlatform = null;
@ -808,7 +1001,6 @@
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.'); RC_DEBUG?.warn('API is enabled — you will be prompted for your bridge key on first command.');
} }
// store interval id so STOP can clear it
this.cleanupIntervalId = setInterval(() => this.cleanupProcessedCommands(), CONFIG.CLEANUP_INTERVAL_MS); this.cleanupIntervalId = setInterval(() => this.cleanupProcessedCommands(), CONFIG.CLEANUP_INTERVAL_MS);
} }
@ -818,12 +1010,11 @@
} }
startObservation() { startObservation() {
// Throttled observer; only rescan if code blocks likely appeared
let scanPending = false; let scanPending = false;
const scheduleScan = () => { const scheduleScan = () => {
if (scanPending) return; if (scanPending) return;
scanPending = true; scanPending = true;
setTimeout(() => { scanPending = false; this.scanMessages(); }, 100); setTimeout(() => { scanPending = false; this.scanMessages(); }, 120);
}; };
this.observer = new MutationObserver((mutations) => { this.observer = new MutationObserver((mutations) => {
@ -839,7 +1030,6 @@
}); });
this.observer.observe(document.body, { childList: true, subtree: true }); this.observer.observe(document.body, { childList: true, subtree: true });
// Respect PROCESS_EXISTING on initial scan (explicitly log skip)
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)');
@ -850,6 +1040,49 @@
} }
} }
// helper: must contain header, action, and final '---' on a line by itself
isCompleteCommandText(txt) {
if (!CONFIG.REQUIRE_TERMINATOR) return /(^|\n)\s*\^%\$bridge\b/m.test(txt) && /(^|\n)\s*action\s*:/m.test(txt);
return /(^|\n)\s*\^%\$bridge\b/m.test(txt)
&& /(^|\n)\s*action\s*:/m.test(txt)
&& /(^|\n)---\s*$/.test(txt);
}
findCommandInCodeBlock(el) {
const blocks = el.querySelectorAll('pre code, pre, code');
for (const b of blocks) {
const txt = (b.textContent || '').trim();
// Only treat as candidate if complete (DeepSeek #2)
if (this.isCompleteCommandText(txt)) {
return { blockElement: b, text: txt };
}
}
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; }
@ -895,28 +1128,6 @@
return true; 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) && /(^|\n)\s*action\s*:/m.test(txt)) {
return { blockElement: b, text: txt };
}
}
return null;
}
getMessageId(element) {
// kept for compatibility
return this.getReadableMessageId(element);
}
reextractCommandText(element) {
const hit = this.findCommandInCodeBlock(element);
return hit ? hit.text : '';
}
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, {
@ -945,51 +1156,46 @@
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; }
// Parsing wrapped for clearer error classification // 1) Initial parse check (strict: must include ---)
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);
// Ignore UI error for common partial/invalid cases if (/No complete \^%\$bridge/.test(err.message)) return; // silent for partials
if (/No valid command block|Missing required field:|YAML parsing error/i.test(err.message)) return;
UIFeedback.appendStatus(message.element, 'ERROR', { action: 'Command', details: err.message }); UIFeedback.appendStatus(message.element, 'ERROR', { action: 'Command', details: err.message });
return; return;
} }
// 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) throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
// 3) Debounce, then settle: ensure final text complete & stable
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.debounce();
const stable = await this.waitForStableCompleteBlock(message.element, before);
if (!stable) { this.updateState(messageId, COMMAND_STATES.ERROR); return; }
const after = this.reextractCommandText(message.element); if (stable !== before) {
if (!after) { this.updateState(messageId, COMMAND_STATES.ERROR); return; } RC_DEBUG?.info('Command changed after settle (re-validate)', { messageId });
message.originalText = stable;
if (after !== before) { const reParsed = CommandParser.parseYAMLCommand(stable);
RC_DEBUG?.info('Command changed during debounce (re-validate)', { messageId }); const reVal = CommandParser.validateStructure(reParsed);
message.originalText = after; if (!reVal.isValid) throw new Error(`Final validation failed: ${reVal.errors.join(', ')}`);
await this.debounce(); parsed = reParsed;
const finalTxt = this.reextractCommandText(message.element);
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(', ')}`);
} }
// Pre-mark to avoid duplicate runs if DOM churns — stays marked even on failure // 4) Pre-mark to avoid duplicate runs even if failure later (explicit design)
this.history.mark(message.originalText); 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 execution reported failure, mark ERROR but do not unmark (no auto-retries).
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; command remains marked (no auto-retry)', { messageId });
this.updateState(messageId, COMMAND_STATES.ERROR); this.updateState(messageId, COMMAND_STATES.ERROR);
@ -1005,11 +1211,9 @@
} catch (error) { } catch (error) {
const duration = Date.now() - started; const duration = Date.now() - started;
RC_DEBUG?.error(`Command processing error: ${error.message}`, { messageId, duration }); RC_DEBUG?.error(`Command processing error: ${error.message}`, { messageId, duration });
// DO NOT unmark — failed commands remain marked to prevent surprise retries
this.updateState(messageId, COMMAND_STATES.ERROR); this.updateState(messageId, COMMAND_STATES.ERROR);
const message = this.trackedMessages.get(messageId); const message = this.trackedMessages.get(messageId);
// Silent ignore for non-commands/partials to avoid noisy inline errors if (/No complete \^%\$bridge/.test(error.message)) return; // quiet for partials
if (/No valid command block|Missing required field:\s*action/i.test(error.message)) return;
if (message) { if (message) {
UIFeedback.appendStatus(message.element, 'ERROR', { action: 'Command', details: error.message }); UIFeedback.appendStatus(message.element, 'ERROR', { action: 'Command', details: error.message });
} }
@ -1043,9 +1247,9 @@
setupEmergencyStop() { setupEmergencyStop() {
window.AI_REPO_STOP = () => { window.AI_REPO_STOP = () => {
// Critical: stop API + pause runtime + cancel inflight + clear interval
CONFIG.ENABLE_API = false; CONFIG.ENABLE_API = false;
CONFIG.RUNTIME.PAUSED = true; CONFIG.RUNTIME.PAUSED = true;
saveConfig(CONFIG);
for (const [id, msg] of this.trackedMessages.entries()) { for (const [id, msg] of this.trackedMessages.entries()) {
if (msg.state === COMMAND_STATES.EXECUTING || msg.state === COMMAND_STATES.DEBOUNCING) { if (msg.state === COMMAND_STATES.EXECUTING || msg.state === COMMAND_STATES.DEBOUNCING) {
@ -1053,17 +1257,11 @@
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 });
}; };
} }
log(...args) {
const [msg, data] = (typeof args[0] === 'string') ? [args[0], args[1]] : ['(log)', args];
RC_DEBUG?.verbose(msg, data);
}
} }
// ---------------------- Manual retry helpers ---------------------- // ---------------------- Manual retry helpers ----------------------
@ -1084,7 +1282,6 @@
if (!msg) { RC_DEBUG?.warn('Message not found for retry', { messageId }); return; } if (!msg) { RC_DEBUG?.warn('Message not found for retry', { messageId }); return; }
commandMonitor.history.unmark(msg.originalText); commandMonitor.history.unmark(msg.originalText);
RC_DEBUG?.info('Message unmarked; reprocessing now', { messageId }); RC_DEBUG?.info('Message unmarked; reprocessing now', { messageId });
// re-run directly (ignores PROCESS_EXISTING and aiRcProcessed flag)
commandMonitor.updateState(messageId, COMMAND_STATES.PARSING); commandMonitor.updateState(messageId, COMMAND_STATES.PARSING);
commandMonitor.processCommand(messageId); commandMonitor.processCommand(messageId);
} catch (e) { } catch (e) {
@ -1092,7 +1289,7 @@
} }
}; };
// ---------------------- Test commands (unchanged) ---------------------- // ---------------------- Test commands ----------------------
const TEST_COMMANDS = { const TEST_COMMANDS = {
validUpdate: validUpdate:
`\ `\
@ -1146,7 +1343,7 @@ path: .
RC_DEBUG?.info('AI Repo Commander fully initialized'); RC_DEBUG?.info('AI Repo Commander fully initialized');
RC_DEBUG?.info('API Enabled:', { value: CONFIG.ENABLE_API }); RC_DEBUG?.info('API Enabled:', { value: CONFIG.ENABLE_API });
RC_DEBUG?.info('Test commands available in window.AI_REPO_COMMANDER.test'); 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()'); RC_DEBUG?.info('Reset history with Tools → Clear History or window.AI_REPO.clearHistory()');
} }
} }