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:
parent
5acd23f595
commit
59379336e2
|
|
@ -1,8 +1,8 @@
|
|||
// ==UserScript==
|
||||
// @name AI Repo Commander
|
||||
// @namespace http://tampermonkey.net/
|
||||
// @version 1.3.4
|
||||
// @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)
|
||||
// @version 1.4.0
|
||||
// @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
|
||||
// @match https://chat.openai.com/*
|
||||
// @match https://chatgpt.com/*
|
||||
|
|
@ -17,40 +17,68 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ---------------------- Config ----------------------
|
||||
const CONFIG = {
|
||||
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.4',
|
||||
// ---------------------- Storage keys ----------------------
|
||||
const STORAGE_KEYS = {
|
||||
history: 'ai_repo_commander_executed',
|
||||
cfg: 'ai_repo_commander_cfg',
|
||||
panel: 'ai_repo_commander_panel_state'
|
||||
};
|
||||
|
||||
PROCESS_EXISTING: false, // If false, only process *new* messages (no initial rescan)
|
||||
ASSISTANT_ONLY: true, // Process assistant messages by default (core use case)
|
||||
// ---------------------- Config (with persistence) ----------------------
|
||||
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
|
||||
DEDUPE_TTL_MS: 30 * 24 * 60 * 60 * 1000, // 30 days
|
||||
|
||||
// Housekeeping
|
||||
CLEANUP_AFTER_MS: 30000, // Drop COMPLETE/ERROR entries after 30s
|
||||
CLEANUP_INTERVAL_MS: 60000, // Sweep cadence
|
||||
CLEANUP_AFTER_MS: 30000,
|
||||
CLEANUP_INTERVAL_MS: 60000,
|
||||
|
||||
// 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, // Delay before submit to let editors settle
|
||||
SUBMIT_MODE: 'button_first', // 'button_first' | 'enter_only' | 'smart'
|
||||
APPEND_TRAILING_NEWLINE: true,
|
||||
AUTO_SUBMIT: true,
|
||||
POST_PASTE_DELAY_MS: 250,
|
||||
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
|
||||
}
|
||||
// Streaming-complete hardening (DeepSeek #2)
|
||||
REQUIRE_TERMINATOR: true, // require trailing '---' line
|
||||
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 ----------------------
|
||||
let RC_DEBUG = null;
|
||||
|
||||
|
|
@ -61,8 +89,12 @@
|
|||
this.loopCounts = new Map();
|
||||
this.startedAt = Date.now();
|
||||
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(() => {
|
||||
if (Date.now() - this.startedAt > this.cfg.DEBUG_WATCH_MS * 2) {
|
||||
this.loopCounts.clear();
|
||||
|
|
@ -74,7 +106,21 @@
|
|||
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
|
||||
|
||||
_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); }
|
||||
warn(msg, data) { this._log(2, 'WARN', msg, data); }
|
||||
info(msg, data) { this._log(3, 'INFO', msg, data); }
|
||||
|
|
@ -89,7 +135,6 @@
|
|||
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;
|
||||
|
|
@ -97,7 +142,6 @@
|
|||
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}`);
|
||||
|
|
@ -134,8 +178,9 @@
|
|||
}
|
||||
|
||||
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;
|
||||
saveConfig(this.cfg);
|
||||
this.info(`Log level => ${lv}`);
|
||||
}
|
||||
|
||||
|
|
@ -155,8 +200,6 @@
|
|||
|
||||
_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;
|
||||
|
|
@ -164,34 +207,34 @@
|
|||
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;
|
||||
}
|
||||
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;
|
||||
position: fixed; ${this.panelState.left!==undefined ? `left:${this.panelState.left}px; top:${this.panelState.top}px;` : 'right:16px; bottom:16px;'}
|
||||
z-index: 2147483647;
|
||||
width: 460px; max-height: 55vh; display: flex; flex-direction: column;
|
||||
background: rgba(20,20,24,0.92); border:1px solid #3b3b46; border-radius: 8px;
|
||||
color:#e5e7eb; font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
box-shadow: 0 16px 40px rgba(0,0,0,0.55); backdrop-filter: blur(4px);
|
||||
`;
|
||||
root.innerHTML = `
|
||||
<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
|
||||
<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</strong>
|
||||
<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;">
|
||||
<option value="0">off</option>
|
||||
<option value="1">errors</option>
|
||||
|
|
@ -200,24 +243,77 @@
|
|||
<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-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-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>
|
||||
</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);
|
||||
this.panel = root;
|
||||
this.bodyLogs = root.querySelector('.rc-body-logs');
|
||||
this.bodyTools = root.querySelector('.rc-body-tools');
|
||||
|
||||
// Controls
|
||||
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));
|
||||
root.querySelector('.rc-copy-200').addEventListener('click', () => this.copyLast(200));
|
||||
|
||||
const pauseBtn = root.querySelector('.rc-pause');
|
||||
pauseBtn.addEventListener('click', () => {
|
||||
this.cfg.RUNTIME.PAUSED = !this.cfg.RUNTIME.PAUSED;
|
||||
saveConfig(this.cfg);
|
||||
pauseBtn.textContent = this.cfg.RUNTIME.PAUSED ? 'Resume' : 'Pause';
|
||||
pauseBtn.style.background = this.cfg.RUNTIME.PAUSED ? '#f59e0b' : '';
|
||||
pauseBtn.style.color = this.cfg.RUNTIME.PAUSED ? '#111827' : '';
|
||||
|
|
@ -228,16 +324,133 @@
|
|||
window.AI_REPO_STOP?.();
|
||||
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) {
|
||||
const body = this.panel.querySelector('.rc-body');
|
||||
if (!this.bodyLogs) return;
|
||||
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;
|
||||
this.bodyLogs.appendChild(row);
|
||||
while (this.bodyLogs.children.length > this.cfg.DEBUG_MAX_LINES) this.bodyLogs.firstChild.remove();
|
||||
this.bodyLogs.scrollTop = this.bodyLogs.scrollHeight;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
|
@ -248,10 +461,10 @@
|
|||
|
||||
// ---------------------- Platform selectors ----------------------
|
||||
const PLATFORM_SELECTORS = {
|
||||
'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' },
|
||||
'claude.ai': { messages: '.chat-message', input: '[contenteditable="true"]', content: '.content' },
|
||||
'gemini.google.com': { messages: '.message-content', input: 'textarea, [contenteditable="true"]', content: '.message-text' }
|
||||
'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' },
|
||||
'claude.ai': { messages: '.chat-message', input: '[contenteditable="true"]', content: '.content' },
|
||||
'gemini.google.com': { messages: '.message-content', input: 'textarea, [contenteditable="true"]', content: '.message-text' }
|
||||
};
|
||||
|
||||
// ---------------------- Command requirements ----------------------
|
||||
|
|
@ -293,7 +506,7 @@
|
|||
// ---------------------- Persistent Command History ----------------------
|
||||
class CommandHistory {
|
||||
constructor() {
|
||||
this.key = 'ai_repo_commander_executed';
|
||||
this.key = STORAGE_KEYS.history;
|
||||
this.ttl = CONFIG.DEDUPE_TTL_MS;
|
||||
this.cleanup();
|
||||
}
|
||||
|
|
@ -318,7 +531,7 @@
|
|||
db[this._hash(text)] = Date.now();
|
||||
this._save(db);
|
||||
}
|
||||
unmark(text) { // manual retry only
|
||||
unmark(text) {
|
||||
const db = this._load();
|
||||
const k = this._hash(text);
|
||||
if (k in db) { delete db[k]; this._save(db); }
|
||||
|
|
@ -334,7 +547,13 @@
|
|||
}
|
||||
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 ----------------------
|
||||
class UIFeedback {
|
||||
|
|
@ -410,12 +629,7 @@
|
|||
const events = ['keydown','keypress','keyup'];
|
||||
for (const type of events) {
|
||||
const ok = el.dispatchEvent(new KeyboardEvent(type, {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
keyCode: 13,
|
||||
which: 13,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true
|
||||
}));
|
||||
if (!ok) return false;
|
||||
}
|
||||
|
|
@ -425,10 +639,7 @@
|
|||
async function submitComposer() {
|
||||
try {
|
||||
const btn = findSendButton();
|
||||
if (CONFIG.SUBMIT_MODE !== 'enter_only' && btn) {
|
||||
btn.click();
|
||||
return true;
|
||||
}
|
||||
if (CONFIG.SUBMIT_MODE !== 'enter_only' && btn) { btn.click(); return true; }
|
||||
const el = getVisibleInputCandidate();
|
||||
if (!el) return false;
|
||||
return pressEnterOn(el);
|
||||
|
|
@ -444,33 +655,25 @@
|
|||
GM_notification({ title: 'AI Repo Commander', text: 'No input box found to paste file content.', timeout: 4000 });
|
||||
return false;
|
||||
}
|
||||
|
||||
const payload = CONFIG.APPEND_TRAILING_NEWLINE ? (text.endsWith('\n') ? text : text + '\n') : text;
|
||||
|
||||
el.focus();
|
||||
|
||||
// 1) ClipboardEvent paste
|
||||
try {
|
||||
const dt = new DataTransfer();
|
||||
dt.setData('text/plain', payload);
|
||||
const pasteEvt = new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true });
|
||||
if (el.dispatchEvent(pasteEvt) && !pasteEvt.defaultPrevented) return true;
|
||||
} catch (_) { /* continue */ }
|
||||
} catch (_) {}
|
||||
|
||||
// 2) execCommand insertText
|
||||
try {
|
||||
const sel = window.getSelection && window.getSelection();
|
||||
if (sel && sel.rangeCount === 0 && el instanceof HTMLElement) {
|
||||
const r = document.createRange();
|
||||
r.selectNodeContents(el);
|
||||
r.collapse(false);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(r);
|
||||
r.selectNodeContents(el); r.collapse(false); sel.removeAllRanges(); sel.addRange(r);
|
||||
}
|
||||
if (document.execCommand && document.execCommand('insertText', false, payload)) return true;
|
||||
} catch (_) { /* continue */ }
|
||||
} catch (_) {}
|
||||
|
||||
// 3) ProseMirror innerHTML
|
||||
const isPM = el.classList && el.classList.contains('ProseMirror');
|
||||
if (isPM) {
|
||||
const escape = (s) => s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
|
|
@ -481,19 +684,13 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
// 4) contenteditable/textarea fallback
|
||||
if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
|
||||
el.value = payload;
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
return true;
|
||||
el.value = payload; el.dispatchEvent(new Event('input', { bubbles: true })); return true;
|
||||
}
|
||||
if (el.isContentEditable || el.getAttribute('contenteditable') === 'true') {
|
||||
el.textContent = payload;
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
return true;
|
||||
el.textContent = payload; el.dispatchEvent(new Event('input', { bubbles: true })); return true;
|
||||
}
|
||||
|
||||
// 5) Clipboard fallback
|
||||
try {
|
||||
if (typeof GM_setClipboard === 'function') {
|
||||
GM_setClipboard(payload, { type: 'text', mimetype: 'text/plain' });
|
||||
|
|
@ -520,11 +717,11 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
// ---------------------- Parser ----------------------
|
||||
// ---------------------- Parser (strict, require ---) ----------------------
|
||||
class CommandParser {
|
||||
static parseYAMLCommand(codeBlockText) {
|
||||
const block = this.extractCommandBlock(codeBlockText);
|
||||
if (!block) throw new Error('No valid command block found');
|
||||
const block = this.extractCompleteBlock(codeBlockText);
|
||||
if (!block) throw new Error('No complete ^%$bridge command found (missing --- terminator).');
|
||||
const parsed = this.parseKeyValuePairs(block);
|
||||
|
||||
// defaults
|
||||
|
|
@ -540,17 +737,14 @@
|
|||
return parsed;
|
||||
}
|
||||
|
||||
static extractCommandBlock(text) {
|
||||
const patterns = [
|
||||
/^\s*\^%\$bridge[ \t]*\n([\s\S]*?)\n---[ \t]*(?:\n|$)/m,
|
||||
/^\s*\^%\$bridge[ \t]*\n([\s\S]*?)(?=\n\s*$|\n---|\n```|$)/m,
|
||||
/^\s*\^%\$bridge[ \t]*\n([\s\S]*)/m
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
const match = text.match(pattern);
|
||||
if (match && match[1]?.trim()) return match[1].trimEnd();
|
||||
}
|
||||
return null;
|
||||
static extractCompleteBlock(text) {
|
||||
// Require terminator line --- (DeepSeek #2)
|
||||
const pattern = /^\s*\^%\$bridge[ \t]*\n([\s\S]*?)\n---[ \t]*(?:\n|$)/m;
|
||||
const m = text.match(pattern);
|
||||
if (!m) return null;
|
||||
const inner = m[1]?.trimEnd();
|
||||
if (!inner) return null;
|
||||
return inner;
|
||||
}
|
||||
|
||||
static parseKeyValuePairs(block) {
|
||||
|
|
@ -580,7 +774,6 @@
|
|||
if (idx !== -1) {
|
||||
const key = line.slice(0, idx).trim();
|
||||
let value = line.slice(idx + 1).trim();
|
||||
|
||||
if (value === '|') {
|
||||
currentKey = key; collecting = true; buf = [];
|
||||
} else if (value === '') {
|
||||
|
|
@ -664,7 +857,7 @@
|
|||
}
|
||||
|
||||
static async mockExecution(command, sourceElement) {
|
||||
await this.delay(1000);
|
||||
await this.delay(500);
|
||||
const mock = {
|
||||
status: 200,
|
||||
responseText: JSON.stringify({
|
||||
|
|
@ -770,10 +963,10 @@
|
|||
return BRIDGE_KEY;
|
||||
}
|
||||
|
||||
// ---------------------- Monitor ----------------------
|
||||
// ---------------------- Monitor (with streaming “settle” & complete-block check) ----------------------
|
||||
class CommandMonitor {
|
||||
constructor() {
|
||||
this.trackedMessages = new Map(); // id -> { element, originalText, state, lastUpdate, startTime }
|
||||
this.trackedMessages = new Map();
|
||||
this.history = new CommandHistory();
|
||||
this.observer = null;
|
||||
this.currentPlatform = null;
|
||||
|
|
@ -808,7 +1001,6 @@
|
|||
if (CONFIG.ENABLE_API) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -818,12 +1010,11 @@
|
|||
}
|
||||
|
||||
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);
|
||||
setTimeout(() => { scanPending = false; this.scanMessages(); }, 120);
|
||||
};
|
||||
|
||||
this.observer = new MutationObserver((mutations) => {
|
||||
|
|
@ -839,7 +1030,6 @@
|
|||
});
|
||||
this.observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// Respect PROCESS_EXISTING on initial scan (explicitly log skip)
|
||||
if (CONFIG.PROCESS_EXISTING) {
|
||||
setTimeout(() => {
|
||||
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() {
|
||||
if (CONFIG.RUNTIME.PAUSED) { RC_DEBUG?.logLoop('loop', 'scan paused'); return; }
|
||||
|
||||
|
|
@ -895,28 +1128,6 @@
|
|||
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) {
|
||||
RC_DEBUG?.info('New command detected', { messageId, preview: text.substring(0, 120) });
|
||||
this.trackedMessages.set(messageId, {
|
||||
|
|
@ -945,51 +1156,46 @@
|
|||
const message = this.trackedMessages.get(messageId);
|
||||
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;
|
||||
try {
|
||||
parsed = CommandParser.parseYAMLCommand(message.originalText);
|
||||
} catch (err) {
|
||||
RC_DEBUG?.error(`Command parsing failed: ${err.message}`, { messageId });
|
||||
this.updateState(messageId, COMMAND_STATES.ERROR);
|
||||
// Ignore UI error for common partial/invalid cases
|
||||
if (/No valid command block|Missing required field:|YAML parsing error/i.test(err.message)) return;
|
||||
if (/No complete \^%\$bridge/.test(err.message)) return; // silent for partials
|
||||
UIFeedback.appendStatus(message.element, 'ERROR', { action: 'Command', details: err.message });
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) Validate
|
||||
this.updateState(messageId, COMMAND_STATES.VALIDATING);
|
||||
|
||||
let validation = CommandParser.validateStructure(parsed);
|
||||
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);
|
||||
const before = message.originalText;
|
||||
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 (!after) { this.updateState(messageId, COMMAND_STATES.ERROR); return; }
|
||||
|
||||
if (after !== before) {
|
||||
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.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(', ')}`);
|
||||
if (stable !== before) {
|
||||
RC_DEBUG?.info('Command changed after settle (re-validate)', { messageId });
|
||||
message.originalText = stable;
|
||||
const reParsed = CommandParser.parseYAMLCommand(stable);
|
||||
const reVal = CommandParser.validateStructure(reParsed);
|
||||
if (!reVal.isValid) throw new Error(`Final validation failed: ${reVal.errors.join(', ')}`);
|
||||
parsed = reParsed;
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// 5) Execute
|
||||
this.updateState(messageId, COMMAND_STATES.EXECUTING);
|
||||
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) {
|
||||
RC_DEBUG?.warn('Execution reported failure; command remains marked (no auto-retry)', { messageId });
|
||||
this.updateState(messageId, COMMAND_STATES.ERROR);
|
||||
|
|
@ -1005,11 +1211,9 @@
|
|||
} catch (error) {
|
||||
const duration = Date.now() - started;
|
||||
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);
|
||||
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 (/No complete \^%\$bridge/.test(error.message)) return; // quiet for partials
|
||||
if (message) {
|
||||
UIFeedback.appendStatus(message.element, 'ERROR', { action: 'Command', details: error.message });
|
||||
}
|
||||
|
|
@ -1043,9 +1247,9 @@
|
|||
|
||||
setupEmergencyStop() {
|
||||
window.AI_REPO_STOP = () => {
|
||||
// Critical: stop API + pause runtime + cancel inflight + clear interval
|
||||
CONFIG.ENABLE_API = false;
|
||||
CONFIG.RUNTIME.PAUSED = true;
|
||||
saveConfig(CONFIG);
|
||||
|
||||
for (const [id, msg] of this.trackedMessages.entries()) {
|
||||
if (msg.state === COMMAND_STATES.EXECUTING || msg.state === COMMAND_STATES.DEBOUNCING) {
|
||||
|
|
@ -1053,17 +1257,11 @@
|
|||
this.updateState(id, COMMAND_STATES.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
this.stopAllProcessing();
|
||||
RC_DEBUG?.error('🚨 EMERGENCY STOP ACTIVATED 🚨');
|
||||
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 ----------------------
|
||||
|
|
@ -1084,7 +1282,6 @@
|
|||
if (!msg) { RC_DEBUG?.warn('Message not found for retry', { messageId }); return; }
|
||||
commandMonitor.history.unmark(msg.originalText);
|
||||
RC_DEBUG?.info('Message unmarked; reprocessing now', { messageId });
|
||||
// re-run directly (ignores PROCESS_EXISTING and aiRcProcessed flag)
|
||||
commandMonitor.updateState(messageId, COMMAND_STATES.PARSING);
|
||||
commandMonitor.processCommand(messageId);
|
||||
} catch (e) {
|
||||
|
|
@ -1092,7 +1289,7 @@
|
|||
}
|
||||
};
|
||||
|
||||
// ---------------------- Test commands (unchanged) ----------------------
|
||||
// ---------------------- Test commands ----------------------
|
||||
const TEST_COMMANDS = {
|
||||
validUpdate:
|
||||
`\
|
||||
|
|
@ -1146,7 +1343,7 @@ path: .
|
|||
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()');
|
||||
RC_DEBUG?.info('Reset history with Tools → Clear History or window.AI_REPO.clearHistory()');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue