Update src/ai-repo-commander.user.js
1. Queue Configuration (lines 59-62) QUEUE_MIN_DELAY_MS: 800 - Minimum delay between command executions QUEUE_MAX_PER_MINUTE: 15 - Rate limiting cap QUEUE_MAX_PER_MESSAGE: 5 - Maximum commands per assistant message QUEUE_WAIT_FOR_COMPOSER_MS: 6000 - Timeout for waiting for composer ready state 2. ExecutionQueue Class (lines 830-862) Self-contained queue with rate limiting Automatically drains commands with proper delays Respects per-minute rate limits Provides size change callbacks for UI updates 3. Multi-Command Detection (lines 328-352) extractAllCompleteBlocks() - Finds all @bridge@ blocks in text findAllCommandsInMessage() - Extracts all commands from a message element attachQueueBadge() - Shows visual indicator of queued commands waitForComposerReady() - Waits for safe state before pasting 4. Enhanced History with Per-Command Deduplication (lines 619-685) Extended fingerprinting with optional suffix for multi-command support Each command in a message gets unique tracking (e.g., #1, #2, etc.) 5. Queue Integration in Scanning (lines 1116-1165) Automatically detects and queues multiple commands Shows badge indicating how many commands were queued Respects QUEUE_MAX_PER_MESSAGE limit enqueueCommand() method handles individual command execution 6. Enhanced Emergency Stop (lines 1323-1345) Clears the entire queue when STOP is activated Reports how many commands were cancelled 7. UI Improvements "Clear Queue" button in header showing current queue size Queue settings in Tools & Settings panel Real-time queue size updates 8. Global API (line 1384) window.AI_REPO_QUEUE.clear() - Clear all queued commands window.AI_REPO_QUEUE.size() - Get current queue size window.AI_REPO_QUEUE.cancelOne(predicate) - Cancel specific command The script now handles multiple commands in a single assistant message gracefully, with proper rate limiting, visual feedback, and robust error handling!
This commit is contained in:
parent
3981fc0528
commit
a65a453feb
|
|
@ -1,8 +1,8 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name AI Repo Commander
|
// @name AI Repo Commander
|
||||||
// @namespace http://tampermonkey.net/
|
// @namespace http://tampermonkey.net/
|
||||||
// @version 1.5.2
|
// @version 1.6.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
|
// @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, multi-command queue
|
||||||
// @author Your Name
|
// @author Your Name
|
||||||
// @match https://chat.openai.com/*
|
// @match https://chat.openai.com/*
|
||||||
// @match https://chatgpt.com/*
|
// @match https://chatgpt.com/*
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
// Timing & API
|
// Timing & API
|
||||||
DEBOUNCE_DELAY: 3000,
|
DEBOUNCE_DELAY: 3000,
|
||||||
MAX_RETRIES: 2,
|
MAX_RETRIES: 2,
|
||||||
VERSION: '1.5.2',
|
VERSION: '1.6.0',
|
||||||
API_TIMEOUT_MS: 60000,
|
API_TIMEOUT_MS: 60000,
|
||||||
|
|
||||||
PROCESS_EXISTING: false,
|
PROCESS_EXISTING: false,
|
||||||
|
|
@ -72,6 +72,12 @@
|
||||||
SCAN_DEBOUNCE_MS: 250,
|
SCAN_DEBOUNCE_MS: 250,
|
||||||
FAST_WARN_MS: 50,
|
FAST_WARN_MS: 50,
|
||||||
SLOW_WARN_MS: 60_000,
|
SLOW_WARN_MS: 60_000,
|
||||||
|
|
||||||
|
// Queue management
|
||||||
|
QUEUE_MIN_DELAY_MS: 800,
|
||||||
|
QUEUE_MAX_PER_MINUTE: 15,
|
||||||
|
QUEUE_MAX_PER_MESSAGE: 5,
|
||||||
|
QUEUE_WAIT_FOR_COMPOSER_MS: 6000,
|
||||||
};
|
};
|
||||||
|
|
||||||
function loadSavedConfig() {
|
function loadSavedConfig() {
|
||||||
|
|
@ -295,6 +301,7 @@
|
||||||
<button class="rc-copy" title="Copy last 50 lines" style="padding:4px 6px;">Copy</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-collapse" title="Collapse/expand" style="padding:4px 6px;">▾</button>
|
||||||
|
<button class="rc-queue-clear" title="Clear command queue" style="padding:4px 6px;background:#7c2d12;color:#fff;border:1px solid #991b1b">Clear Queue (0)</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 rc-body-logs" 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>
|
||||||
|
|
@ -338,6 +345,21 @@
|
||||||
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;">Queue Settings</h4>
|
||||||
|
<label style="display:flex;align-items:center;gap:8px;margin:4px 0;">
|
||||||
|
QUEUE_MIN_DELAY_MS <input class="rc-num" data-key="QUEUE_MIN_DELAY_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;">
|
||||||
|
QUEUE_MAX_PER_MINUTE <input class="rc-num" data-key="QUEUE_MAX_PER_MINUTE" type="number" min="1" step="1" 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;">
|
||||||
|
QUEUE_MAX_PER_MESSAGE <input class="rc-num" data-key="QUEUE_MAX_PER_MESSAGE" type="number" min="1" step="1" 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;">
|
||||||
|
QUEUE_WAIT_FOR_COMPOSER_MS <input class="rc-num" data-key="QUEUE_WAIT_FOR_COMPOSER_MS" type="number" min="1000" step="500" 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;">
|
<div style="grid-column:1 / -1;">
|
||||||
<h4 style="margin:8px 0 6px 0;">Bridge Configuration</h4>
|
<h4 style="margin:8px 0 6px 0;">Bridge Configuration</h4>
|
||||||
<label style="display:flex;align-items:center;gap:8px;margin:4px 0;">
|
<label style="display:flex;align-items:center;gap:8px;margin:4px 0;">
|
||||||
|
|
@ -402,6 +424,15 @@
|
||||||
this.info(`Runtime ${this.cfg.RUNTIME.PAUSED ? 'paused' : 'resumed'}`);
|
this.info(`Runtime ${this.cfg.RUNTIME.PAUSED ? 'paused' : 'resumed'}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Queue clear button
|
||||||
|
const queueBtn = root.querySelector('.rc-queue-clear');
|
||||||
|
queueBtn.addEventListener('click', (e) => {
|
||||||
|
window.AI_REPO_QUEUE?.clear?.();
|
||||||
|
this.flashBtn(e.currentTarget, 'Cleared');
|
||||||
|
this.toast('Queue cleared');
|
||||||
|
this.warn('Command queue cleared');
|
||||||
|
});
|
||||||
|
|
||||||
root.querySelector('.rc-stop').addEventListener('click', (e) => {
|
root.querySelector('.rc-stop').addEventListener('click', (e) => {
|
||||||
window.AI_REPO_STOP?.();
|
window.AI_REPO_STOP?.();
|
||||||
this.flashBtn(e.currentTarget, 'Stopped');
|
this.flashBtn(e.currentTarget, 'Stopped');
|
||||||
|
|
@ -727,6 +758,63 @@
|
||||||
return fingerprint;
|
return fingerprint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------- Multi-block extraction helpers ----------------------
|
||||||
|
function extractAllCompleteBlocks(text) {
|
||||||
|
const out = [];
|
||||||
|
const re = /^\s*@bridge@[ \t]*\n([\s\S]*?)\n@end@[ \t]*(?:\n|$)/gm;
|
||||||
|
let m;
|
||||||
|
while ((m = re.exec(text)) !== null) {
|
||||||
|
const inner = (m[1] || '').trimEnd();
|
||||||
|
if (inner && /(^|\n)\s*action\s*:/m.test(inner)) out.push(inner);
|
||||||
|
}
|
||||||
|
return out; // array of inner texts (without @bridge@/@end@)
|
||||||
|
}
|
||||||
|
|
||||||
|
function findAllCommandsInMessage(el) {
|
||||||
|
const blocks = el.querySelectorAll('pre code, pre, code');
|
||||||
|
const hits = [];
|
||||||
|
for (const b of blocks) {
|
||||||
|
const txt = (b.textContent || '').trim();
|
||||||
|
const parts = extractAllCompleteBlocks(txt);
|
||||||
|
for (const part of parts) hits.push({ blockElement: b, text: `@bridge@\n${part}\n@end@` });
|
||||||
|
}
|
||||||
|
return hits;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tiny badge on the message showing how many got queued
|
||||||
|
function attachQueueBadge(el, count) {
|
||||||
|
if (el.querySelector('.ai-rc-queue-badge')) return;
|
||||||
|
const badge = document.createElement('span');
|
||||||
|
badge.className = 'ai-rc-queue-badge';
|
||||||
|
badge.textContent = `${count} command${count>1?'s':''} queued`;
|
||||||
|
badge.style.cssText = `
|
||||||
|
display:inline-block; padding:2px 6px; margin:4px 0;
|
||||||
|
background:#3b82f6; color:#fff; border-radius:4px;
|
||||||
|
font:11px ui-monospace, monospace;`;
|
||||||
|
el.insertBefore(badge, el.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until it's safe to paste/submit
|
||||||
|
async function waitForComposerReady({ timeoutMs = CONFIG.QUEUE_WAIT_FOR_COMPOSER_MS, pollMs = 200 } = {}) {
|
||||||
|
const start = Date.now();
|
||||||
|
while (Date.now() - start < timeoutMs) {
|
||||||
|
// basic "assistant still typing" check
|
||||||
|
const lastMsg = Array.from(document.querySelectorAll(MSG_SELECTORS.join(','))).pop();
|
||||||
|
if (lastMsg?.querySelector?.('[aria-busy="true"], .typing-indicator')) {
|
||||||
|
await ExecutionManager.delay(400);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const el = getVisibleInputCandidate();
|
||||||
|
const btn = findSendButton();
|
||||||
|
const btnReady = !btn || (!btn.disabled && btn.getAttribute('aria-disabled') !== 'true');
|
||||||
|
const busy = document.querySelector('[aria-busy="true"], [data-state="loading"], [disabled]');
|
||||||
|
if (el && btnReady && !busy) return true;
|
||||||
|
await ExecutionManager.delay(pollMs);
|
||||||
|
}
|
||||||
|
RC_DEBUG?.warn('Composer not ready within timeout');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------- Conversation-Aware Element History ----------------------
|
// ---------------------- Conversation-Aware Element History ----------------------
|
||||||
function getConversationId() {
|
function getConversationId() {
|
||||||
const host = location.hostname.replace('chat.openai.com', 'chatgpt.com'); // normalize
|
const host = location.hostname.replace('chat.openai.com', 'chatgpt.com'); // normalize
|
||||||
|
|
@ -887,8 +975,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hasElement(el) {
|
hasElement(el, suffix = '') {
|
||||||
const fp = fingerprintElement(el);
|
let fp = fingerprintElement(el);
|
||||||
|
if (suffix) fp += `#${suffix}`;
|
||||||
const result = this.session.has(fp) || (fp in this.cache);
|
const result = this.session.has(fp) || (fp in this.cache);
|
||||||
|
|
||||||
if (result && CONFIG.DEBUG_LEVEL >= 4) {
|
if (result && CONFIG.DEBUG_LEVEL >= 4) {
|
||||||
|
|
@ -902,8 +991,9 @@
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
markElement(el) {
|
markElement(el, suffix = '') {
|
||||||
const fp = fingerprintElement(el);
|
let fp = fingerprintElement(el);
|
||||||
|
if (suffix) fp += `#${suffix}`;
|
||||||
this.session.add(fp);
|
this.session.add(fp);
|
||||||
this.cache[fp] = Date.now();
|
this.cache[fp] = Date.now();
|
||||||
this._save();
|
this._save();
|
||||||
|
|
@ -920,8 +1010,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unmarkElement(el) {
|
unmarkElement(el, suffix = '') {
|
||||||
const fp = fingerprintElement(el);
|
let fp = fingerprintElement(el);
|
||||||
|
if (suffix) fp += `#${suffix}`;
|
||||||
this.session.delete(fp);
|
this.session.delete(fp);
|
||||||
if (fp in this.cache) {
|
if (fp in this.cache) {
|
||||||
delete this.cache[fp];
|
delete this.cache[fp];
|
||||||
|
|
@ -1174,6 +1265,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pasteAndMaybeSubmit(text) {
|
async function pasteAndMaybeSubmit(text) {
|
||||||
|
const ready = await waitForComposerReady({ timeoutMs: CONFIG.QUEUE_WAIT_FOR_COMPOSER_MS });
|
||||||
|
if (!ready) {
|
||||||
|
RC_DEBUG?.warn('Composer not ready; re-queueing paste');
|
||||||
|
execQueue.push(async () => { await pasteAndMaybeSubmit(text); });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const pasted = pasteToComposer(text);
|
const pasted = pasteToComposer(text);
|
||||||
if (!pasted) return false;
|
if (!pasted) return false;
|
||||||
|
|
||||||
|
|
@ -1239,8 +1337,6 @@
|
||||||
let currentKey = null;
|
let currentKey = null;
|
||||||
let collecting = false;
|
let collecting = false;
|
||||||
let buf = [];
|
let buf = [];
|
||||||
// Kept for reference, but no longer used for collection termination
|
|
||||||
const TOP = ['action','repo','path','content','owner','url','commit_message','branch','ref'];
|
|
||||||
|
|
||||||
for (const raw of lines) {
|
for (const raw of lines) {
|
||||||
const line = raw.replace(/\r$/, '');
|
const line = raw.replace(/\r$/, '');
|
||||||
|
|
@ -1452,6 +1548,60 @@
|
||||||
static delay(ms) { return new Promise(r => setTimeout(r, ms)); }
|
static delay(ms) { return new Promise(r => setTimeout(r, ms)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------- Execution Queue ----------------------
|
||||||
|
class ExecutionQueue {
|
||||||
|
constructor({ minDelayMs = CONFIG.QUEUE_MIN_DELAY_MS, maxPerMinute = CONFIG.QUEUE_MAX_PER_MINUTE } = {}) {
|
||||||
|
this.q = [];
|
||||||
|
this.running = false;
|
||||||
|
this.minDelayMs = minDelayMs;
|
||||||
|
this.maxPerMinute = maxPerMinute;
|
||||||
|
this.timestamps = [];
|
||||||
|
this.onSizeChange = null;
|
||||||
|
}
|
||||||
|
push(task) {
|
||||||
|
this.q.push(task);
|
||||||
|
this.onSizeChange?.(this.q.length);
|
||||||
|
if (!this.running) this._drain();
|
||||||
|
}
|
||||||
|
clear() {
|
||||||
|
this.q.length = 0;
|
||||||
|
this.onSizeChange?.(0);
|
||||||
|
}
|
||||||
|
cancelOne(predicate) {
|
||||||
|
const i = this.q.findIndex(predicate);
|
||||||
|
if (i >= 0) this.q.splice(i, 1);
|
||||||
|
this.onSizeChange?.(this.q.length);
|
||||||
|
}
|
||||||
|
_withinBudget() {
|
||||||
|
const now = Date.now();
|
||||||
|
this.timestamps = this.timestamps.filter(t => now - t < 60_000);
|
||||||
|
return this.timestamps.length < this.maxPerMinute;
|
||||||
|
}
|
||||||
|
async _drain() {
|
||||||
|
if (this.running) return;
|
||||||
|
this.running = true;
|
||||||
|
const origLen = this.q.length;
|
||||||
|
while (this.q.length) {
|
||||||
|
// rate cap
|
||||||
|
while (!this._withinBudget()) await ExecutionManager.delay(500);
|
||||||
|
const fn = this.q.shift();
|
||||||
|
this.onSizeChange?.(this.q.length);
|
||||||
|
RC_DEBUG?.toast?.(`Executing command ${origLen - this.q.length}/${origLen}`, 800);
|
||||||
|
try { await fn(); } catch { /* error already surfaced */ }
|
||||||
|
this.timestamps.push(Date.now());
|
||||||
|
await ExecutionManager.delay(this.minDelayMs);
|
||||||
|
}
|
||||||
|
this.running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const execQueue = new ExecutionQueue();
|
||||||
|
window.AI_REPO_QUEUE = {
|
||||||
|
clear: () => execQueue.clear(),
|
||||||
|
size: () => execQueue.q.length,
|
||||||
|
cancelOne: (cb) => execQueue.cancelOne(cb),
|
||||||
|
};
|
||||||
|
|
||||||
// ---------------------- Bridge Key ----------------------
|
// ---------------------- Bridge Key ----------------------
|
||||||
let BRIDGE_KEY = null;
|
let BRIDGE_KEY = null;
|
||||||
|
|
||||||
|
|
@ -1544,6 +1694,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.cleanupIntervalId = setInterval(() => this.cleanupProcessedCommands(), CONFIG.CLEANUP_INTERVAL_MS);
|
this.cleanupIntervalId = setInterval(() => this.cleanupProcessedCommands(), CONFIG.CLEANUP_INTERVAL_MS);
|
||||||
|
|
||||||
|
// Wire up queue size updates to UI
|
||||||
|
if (execQueue) {
|
||||||
|
execQueue.onSizeChange = (n) => {
|
||||||
|
const queueBtn = document.querySelector('.rc-queue-clear');
|
||||||
|
if (queueBtn) queueBtn.textContent = `Clear Queue (${n})`;
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
detectPlatform() {
|
detectPlatform() {
|
||||||
|
|
@ -1687,77 +1845,111 @@
|
||||||
if (!this.isAssistantMessage(el)) return;
|
if (!this.isAssistantMessage(el)) return;
|
||||||
if (el.dataset.aiRcProcessed) return;
|
if (el.dataset.aiRcProcessed) return;
|
||||||
|
|
||||||
const hit = this.findCommandInCodeBlock(el);
|
const hits = findAllCommandsInMessage(el);
|
||||||
if (!hit) return;
|
if (!hits.length) return;
|
||||||
|
|
||||||
const cmdText = hit.text;
|
if (hits.length === 1) {
|
||||||
const withinColdStart = Date.now() < this.coldStartUntil;
|
|
||||||
const alreadyProcessed = this.history.hasElement(el);
|
|
||||||
|
|
||||||
RC_DEBUG?.trace('Evaluating message', {
|
|
||||||
withinColdStart,
|
|
||||||
alreadyProcessed,
|
|
||||||
preview: cmdText.slice(0, 60)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Skip if cold start (but DON'T mark in history)
|
|
||||||
if (withinColdStart) {
|
|
||||||
el.dataset.aiRcProcessed = '1';
|
el.dataset.aiRcProcessed = '1';
|
||||||
|
if (this.history.hasElement(el, 1)) {
|
||||||
RC_DEBUG?.verbose('Skipping command - page load (cold start)', {
|
attachRunAgainUI(el, () => this.trackMessage(el, hits[0].text, this.getReadableMessageId(el)));
|
||||||
fingerprint: fingerprintElement(el).slice(0, 40) + '...',
|
return;
|
||||||
preview: cmdText.slice(0, 80)
|
|
||||||
});
|
|
||||||
|
|
||||||
attachRunAgainUI(el, () => {
|
|
||||||
el.dataset.aiRcProcessed = '1';
|
|
||||||
|
|
||||||
const id = this.getReadableMessageId(el);
|
|
||||||
const hit2 = this.findCommandInCodeBlock(el);
|
|
||||||
if (hit2) {
|
|
||||||
this.trackMessage(el, hit2.text, id);
|
|
||||||
}
|
}
|
||||||
});
|
this.history.markElement(el, 1);
|
||||||
|
this.trackMessage(el, hits[0].text, this.getReadableMessageId(el));
|
||||||
skipped++;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if already processed in this conversation
|
const withinColdStart = Date.now() < this.coldStartUntil;
|
||||||
if (alreadyProcessed) {
|
const alreadyAll = hits.every((_, i) => this.history.hasElement(el, i + 1));
|
||||||
|
|
||||||
|
RC_DEBUG?.trace('Evaluating message', {
|
||||||
|
withinColdStart,
|
||||||
|
alreadyAll,
|
||||||
|
commandCount: hits.length
|
||||||
|
});
|
||||||
|
|
||||||
|
// Skip if cold start or already processed (but DON'T mark new ones in history during cold start)
|
||||||
|
if (withinColdStart || alreadyAll) {
|
||||||
el.dataset.aiRcProcessed = '1';
|
el.dataset.aiRcProcessed = '1';
|
||||||
|
|
||||||
RC_DEBUG?.verbose('Skipping command - already executed in this conversation', {
|
RC_DEBUG?.verbose('Skipping command(s) - ' + (withinColdStart ? 'page load (cold start)' : 'already executed in this conversation'), {
|
||||||
fingerprint: fingerprintElement(el).slice(0, 40) + '...',
|
fingerprint: fingerprintElement(el).slice(0, 40) + '...',
|
||||||
preview: cmdText.slice(0, 80)
|
commandCount: hits.length
|
||||||
});
|
});
|
||||||
|
|
||||||
attachRunAgainUI(el, () => {
|
attachRunAgainUI(el, () => {
|
||||||
el.dataset.aiRcProcessed = '1';
|
el.dataset.aiRcProcessed = '1';
|
||||||
const id = this.getReadableMessageId(el);
|
const hit2 = findAllCommandsInMessage(el);
|
||||||
const hit2 = this.findCommandInCodeBlock(el);
|
if (hit2.length) {
|
||||||
if (hit2) {
|
const capped = hit2.slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE);
|
||||||
this.trackMessage(el, hit2.text, id);
|
capped.forEach((h, i) => this.enqueueCommand(el, h, i));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
skipped++;
|
skipped += hits.length;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// New message that hasn't been executed → auto-execute once
|
// New message that hasn't been executed → auto-execute once
|
||||||
el.dataset.aiRcProcessed = '1';
|
el.dataset.aiRcProcessed = '1';
|
||||||
this.history.markElement(el);
|
const capped = hits.slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE);
|
||||||
|
attachQueueBadge(el, capped.length);
|
||||||
|
|
||||||
const id = this.getReadableMessageId(el);
|
capped.forEach((hit, idx) => {
|
||||||
this.trackMessage(el, cmdText, id);
|
// mark each sub-command immediately to avoid re-exec on reloads
|
||||||
found++;
|
this.history.markElement(el, idx + 1);
|
||||||
|
this.enqueueCommand(el, hit, idx);
|
||||||
|
});
|
||||||
|
found += capped.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (skipped) RC_DEBUG?.info(`Skipped ${skipped} command(s) - Run Again buttons added`);
|
if (skipped) RC_DEBUG?.info(`Skipped ${skipped} command(s) - Run Again buttons added`);
|
||||||
if (found) RC_DEBUG?.info(`Auto-executing ${found} new command(s)`);
|
if (found) RC_DEBUG?.info(`Auto-executing ${found} new command(s)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enqueueCommand(element, hit, idx) {
|
||||||
|
const messageId = this.getReadableMessageId(element);
|
||||||
|
const subId = `${messageId}#${idx + 1}`;
|
||||||
|
|
||||||
|
// track this sub-command so updateState/attachRetryUI can work
|
||||||
|
this.trackedMessages.set(subId, {
|
||||||
|
element,
|
||||||
|
originalText: hit.text,
|
||||||
|
state: COMMAND_STATES.DETECTED,
|
||||||
|
startTime: Date.now(),
|
||||||
|
lastUpdate: Date.now(),
|
||||||
|
cancelToken: { cancelled: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
execQueue.push(async () => {
|
||||||
|
// optional tiny settle for streaming
|
||||||
|
await ExecutionManager.delay(CONFIG.SETTLE_POLL_MS);
|
||||||
|
|
||||||
|
const allNow = findAllCommandsInMessage(element);
|
||||||
|
const liveForIdx = allNow[idx]?.text;
|
||||||
|
const finalTxt = (liveForIdx && this.isCompleteCommandText(liveForIdx)) ? liveForIdx : hit.text;
|
||||||
|
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = CommandParser.parseYAMLCommand(finalTxt);
|
||||||
|
const val = CommandParser.validateStructure(parsed);
|
||||||
|
if (!val.isValid) throw new Error(`Validation failed: ${val.errors.join(', ')}`);
|
||||||
|
} catch (err) {
|
||||||
|
UIFeedback.appendStatus(element, 'ERROR', { action: 'Command', details: err.message });
|
||||||
|
this.attachRetryUI(element, subId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateState(subId, COMMAND_STATES.EXECUTING);
|
||||||
|
const res = await ExecutionManager.executeCommand(parsed, element);
|
||||||
|
if (!res || res.success === false) {
|
||||||
|
this.updateState(subId, COMMAND_STATES.ERROR);
|
||||||
|
this.attachRetryUI(element, subId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.updateState(subId, COMMAND_STATES.COMPLETE);
|
||||||
|
});
|
||||||
|
}
|
||||||
isAssistantMessage(el) {
|
isAssistantMessage(el) {
|
||||||
if (!CONFIG.ASSISTANT_ONLY) return true;
|
if (!CONFIG.ASSISTANT_ONLY) return true;
|
||||||
const host = location.hostname;
|
const host = location.hostname;
|
||||||
|
|
@ -1842,15 +2034,22 @@
|
||||||
|
|
||||||
attachRunAgainUI(element, () => {
|
attachRunAgainUI(element, () => {
|
||||||
element.dataset.aiRcProcessed = '1';
|
element.dataset.aiRcProcessed = '1';
|
||||||
const hit = this.findCommandInCodeBlock(element);
|
|
||||||
if (hit) {
|
// Parse sub-index from messageId like "...#3"
|
||||||
|
const m = /#(\d+)$/.exec(messageId);
|
||||||
|
const wantIdx = m ? Math.max(0, parseInt(m[1], 10) - 1) : 0;
|
||||||
|
|
||||||
|
const all = findAllCommandsInMessage(element);
|
||||||
|
const selected = all[wantIdx]?.text || all[0]?.text;
|
||||||
|
if (selected) {
|
||||||
this.trackedMessages.delete(messageId);
|
this.trackedMessages.delete(messageId);
|
||||||
const newId = this.getReadableMessageId(element);
|
const newId = this.getReadableMessageId(element);
|
||||||
this.trackMessage(element, hit.text, newId);
|
this.trackMessage(element, selected, 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;
|
||||||
|
|
@ -2011,6 +2210,10 @@
|
||||||
CONFIG.RUNTIME.PAUSED = true;
|
CONFIG.RUNTIME.PAUSED = true;
|
||||||
saveConfig(CONFIG);
|
saveConfig(CONFIG);
|
||||||
|
|
||||||
|
const queuedCount = execQueue.q.length;
|
||||||
|
execQueue.clear();
|
||||||
|
RC_DEBUG?.error(`🚨 EMERGENCY STOP: cancelled ${queuedCount} queued command(s)`);
|
||||||
|
|
||||||
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.cancelToken) msg.cancelToken.cancelled = true;
|
||||||
|
|
||||||
|
|
@ -2161,6 +2364,28 @@ body: |
|
||||||
- Fix Y
|
- Fix Y
|
||||||
@end@
|
@end@
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
`,
|
||||||
|
multiCommand:
|
||||||
|
`\
|
||||||
|
\`\`\`yaml
|
||||||
|
@bridge@
|
||||||
|
action: get_file
|
||||||
|
repo: test-repo
|
||||||
|
path: file1.txt
|
||||||
|
@end@
|
||||||
|
|
||||||
|
@bridge@
|
||||||
|
action: get_file
|
||||||
|
repo: test-repo
|
||||||
|
path: file2.txt
|
||||||
|
@end@
|
||||||
|
|
||||||
|
@bridge@
|
||||||
|
action: list_files
|
||||||
|
repo: test-repo
|
||||||
|
path: .
|
||||||
|
@end@
|
||||||
|
\`\`\`
|
||||||
`
|
`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -2176,12 +2401,14 @@ body: |
|
||||||
test: TEST_COMMANDS,
|
test: TEST_COMMANDS,
|
||||||
version: CONFIG.VERSION,
|
version: CONFIG.VERSION,
|
||||||
history: commandMonitor.history,
|
history: commandMonitor.history,
|
||||||
submitComposer
|
submitComposer,
|
||||||
|
queue: execQueue
|
||||||
};
|
};
|
||||||
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 Tools → Clear History or window.AI_REPO.clearHistory()');
|
RC_DEBUG?.info('Reset history with Tools → Clear History or window.AI_REPO.clearHistory()');
|
||||||
|
RC_DEBUG?.info('Queue management: window.AI_REPO_QUEUE.clear() / .size() / .cancelOne()');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue