Update src/ai-repo-commander.user.js
Double-execution / wrong “Run again” target: You now fingerprint per message and per sub-command (markElement(el, idx+1)) and render per-command “Run again [#n]” buttons. That removes the old single-block ambiguity and prevents re-exec on reloads. Cold-start / resume spam: On resume you call markExistingHitsAsProcessed() which marks visible commands and injects Run-again buttons instead of auto-executing. That stops surprise replays after page refresh or unpause. Streaming “half blocks” firing too early: waitForStableCompleteBlock() + the required @end@ terminator and the settle window (SETTLE_CHECK_MS/SETTLE_POLL_MS) means you only act on a fully streamed, stable block. Multi-command messages: extractAllCompleteBlocks() + queueing each hit with unique render keys/labels gives correct detection, dedupe, status lines, and retries per block. YAML | blocks cutting off at new keys: The parser now ends a | block on any unindented key:—fixing prior cases where multi-line fields (content/body) swallowed following keys or vice-versa. Composer/paste flakiness across models: You added a robust paste stack (ClipboardEvent → ProseMirror HTML → execCommand → textarea/value → contentEditable → GM_setClipboard fallback) and waitForComposerReady() with send-button checks. That should address “pasted but empty” and “submit didn’t fire”. Long response pastes breaking fences / hitting limits: ResponseBuffer consolidates outputs, and splitRespectingCodeFence() keeps single fenced blobs intact across chunks while honoring MAX_PASTE_CHARS. History that didn’t survive reloads / wrong thread: Conversation-scoped persistent dedupe (ConvHistory with TTL) plus a stronger fingerprint (command hash + previous-context hash + DOM hint + ordinal) prevents cross-thread collisions and stale replays. Emergency stop + rate limiting: Queue rate-cap + STOP handler will keep you safe from API storms and let you halt mid-flight cleanly.
This commit is contained in:
parent
dd0427b598
commit
306482a281
|
|
@ -12,6 +12,7 @@
|
||||||
// @grant GM_notification
|
// @grant GM_notification
|
||||||
// @grant GM_setClipboard
|
// @grant GM_setClipboard
|
||||||
// @connect n8n.brrd.tech
|
// @connect n8n.brrd.tech
|
||||||
|
// @connect *
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
|
|
@ -37,7 +38,7 @@
|
||||||
// If you see "debouncing → error" in logs (assistant streams very slowly),
|
// If you see "debouncing → error" in logs (assistant streams very slowly),
|
||||||
// try bumping DEBOUNCE_DELAY by +1000–2000 and/or SETTLE_CHECK_MS by +400–800.
|
// try bumping DEBOUNCE_DELAY by +1000–2000 and/or SETTLE_CHECK_MS by +400–800.
|
||||||
|
|
||||||
DEBOUNCE_DELAY: 3000,
|
DEBOUNCE_DELAY: 6500,
|
||||||
MAX_RETRIES: 2,
|
MAX_RETRIES: 2,
|
||||||
VERSION: '1.6.2',
|
VERSION: '1.6.2',
|
||||||
API_TIMEOUT_MS: 60000,
|
API_TIMEOUT_MS: 60000,
|
||||||
|
|
@ -66,15 +67,15 @@
|
||||||
// SETTLE_CHECK_MS is the "stable window" after last text change;
|
// SETTLE_CHECK_MS is the "stable window" after last text change;
|
||||||
// SETTLE_POLL_MS is how often we re-check the code block.
|
// SETTLE_POLL_MS is how often we re-check the code block.
|
||||||
REQUIRE_TERMINATOR: true,
|
REQUIRE_TERMINATOR: true,
|
||||||
SETTLE_CHECK_MS: 800,
|
SETTLE_CHECK_MS: 1300,
|
||||||
SETTLE_POLL_MS: 200,
|
SETTLE_POLL_MS: 250,
|
||||||
|
|
||||||
// Runtime toggles
|
// Runtime toggles
|
||||||
RUNTIME: { PAUSED: false },
|
RUNTIME: { PAUSED: false },
|
||||||
|
|
||||||
// New additions for hardening
|
// New additions for hardening
|
||||||
STUCK_AFTER_MS: 10 * 60 * 1000,
|
STUCK_AFTER_MS: 10 * 60 * 1000,
|
||||||
SCAN_DEBOUNCE_MS: 250,
|
SCAN_DEBOUNCE_MS: 400,
|
||||||
FAST_WARN_MS: 50,
|
FAST_WARN_MS: 50,
|
||||||
SLOW_WARN_MS: 60_000,
|
SLOW_WARN_MS: 60_000,
|
||||||
|
|
||||||
|
|
@ -507,7 +508,8 @@
|
||||||
// Dragging
|
// Dragging
|
||||||
const header = root.querySelector('.rc-header');
|
const header = root.querySelector('.rc-header');
|
||||||
header.addEventListener('mousedown', (e) => {
|
header.addEventListener('mousedown', (e) => {
|
||||||
if ((e.target).closest('button,select,input,textarea,label')) return;
|
const tgt = e.target instanceof Element ? e.target : e.target?.parentElement;
|
||||||
|
if (tgt?.closest('button,select,input,textarea,label')) return;
|
||||||
this.drag.active = true;
|
this.drag.active = true;
|
||||||
const rect = root.getBoundingClientRect();
|
const rect = root.getBoundingClientRect();
|
||||||
this.drag.dx = e.clientX - rect.left;
|
this.drag.dx = e.clientX - rect.left;
|
||||||
|
|
@ -828,12 +830,10 @@
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const el = getVisibleInputCandidate();
|
const el = getVisibleInputCandidate();
|
||||||
const btn = findSendButton();
|
const btn = findSendButton(el);
|
||||||
const btnReady = !btn || (!btn.disabled && btn.getAttribute('aria-disabled') !== 'true');
|
const btnReady = !btn || (!btn.disabled && btn.getAttribute('aria-disabled') !== 'true');
|
||||||
const busy = document.querySelector(
|
const scope = el?.closest('form, [data-testid="composer"], main, body') || document;
|
||||||
'[aria-busy="true"], [data-state="loading"], ' +
|
const busy = scope.querySelector('[aria-busy="true"], [data-state="loading"]');
|
||||||
'button[disabled], button[aria-disabled="true"]'
|
|
||||||
);
|
|
||||||
if (el && btnReady && !busy) return true;
|
if (el && btnReady && !busy) return true;
|
||||||
await ExecutionManager.delay(pollMs);
|
await ExecutionManager.delay(pollMs);
|
||||||
}
|
}
|
||||||
|
|
@ -918,8 +918,8 @@
|
||||||
repo: (v) => /^[\w\-\.]+(\/[\w\-\.]+)?$/.test(v),
|
repo: (v) => /^[\w\-\.]+(\/[\w\-\.]+)?$/.test(v),
|
||||||
path: (v) => !v.includes('..') && !v.startsWith('/') && !v.includes('\\'),
|
path: (v) => !v.includes('..') && !v.startsWith('/') && !v.includes('\\'),
|
||||||
action: (v) => Object.keys(REQUIRED_FIELDS).includes(v),
|
action: (v) => Object.keys(REQUIRED_FIELDS).includes(v),
|
||||||
owner: (v) => !v || /^[\w\-]+$/.test(v),
|
url: (v) => !v || /^https?:\/\/[^/\s]+(?:\/|$)/i.test(v),
|
||||||
url: (v) => !v || /^https?:\/\/.+\..+/.test(v),
|
owner: (v) => !v || /^[\w\-.]+$/.test(v),
|
||||||
branch: (v) => v && v.length > 0 && !v.includes('..'),
|
branch: (v) => v && v.length > 0 && !v.includes('..'),
|
||||||
source_branch:(v) => !v || (v.length > 0 && !v.includes('..')),
|
source_branch:(v) => !v || (v.length > 0 && !v.includes('..')),
|
||||||
head: (v) => v && v.length > 0,
|
head: (v) => v && v.length > 0,
|
||||||
|
|
@ -1068,26 +1068,85 @@
|
||||||
setConfig: (patch) => { Object.assign(CONFIG, patch||{}); saveConfig(CONFIG); },
|
setConfig: (patch) => { Object.assign(CONFIG, patch||{}); saveConfig(CONFIG); },
|
||||||
};
|
};
|
||||||
|
|
||||||
function attachRunAgainUI(containerEl, onRun) {
|
// Replace the whole attachRunAgainUI with this per-command version (and keep a thin wrapper for back-compat)
|
||||||
if (containerEl.querySelector('.ai-rc-rerun')) return;
|
function attachRunAgainPerCommand(containerEl, hits, onRunOneIdx, onRunAll) {
|
||||||
|
// Rebuild if an old single-button bar exists
|
||||||
|
const old = containerEl.querySelector('.ai-rc-rerun');
|
||||||
|
if (old) old.remove();
|
||||||
|
|
||||||
const bar = document.createElement('div');
|
const bar = document.createElement('div');
|
||||||
bar.className = 'ai-rc-rerun';
|
bar.className = 'ai-rc-rerun';
|
||||||
bar.style.cssText = 'margin:8px 0; display:flex; gap:8px; align-items:center;';
|
bar.style.cssText = 'margin:8px 0; display:flex; gap:8px; align-items:center; flex-wrap:wrap;';
|
||||||
|
|
||||||
const msg = document.createElement('span');
|
const msg = document.createElement('span');
|
||||||
msg.textContent = 'Already executed.';
|
msg.textContent = `Already executed. Re-run:`;
|
||||||
msg.style.cssText = 'flex:1; font-size:13px; opacity:.9;';
|
msg.style.cssText = 'font-size:13px; opacity:.9; margin-right:6px;';
|
||||||
const run = document.createElement('button');
|
bar.appendChild(msg);
|
||||||
run.textContent = 'Run again';
|
// "Run all again" button (optional legacy support)
|
||||||
run.style.cssText = 'padding:4px 10px; border:1px solid #374151; border-radius:4px; background:#1f2937; color:#e5e7eb;';
|
const runAllBtn = document.createElement('button');
|
||||||
|
runAllBtn.textContent = 'Run all again';
|
||||||
|
runAllBtn.style.cssText = 'padding:4px 10px; border:1px solid #374151; border-radius:4px; background:#1f2937; color:#e5e7eb;';
|
||||||
|
runAllBtn.addEventListener('click', (ev) => {
|
||||||
|
RC_DEBUG?.flashBtn?.(ev.currentTarget, 'Running');
|
||||||
|
try {
|
||||||
|
if (typeof onRunAll === 'function') {
|
||||||
|
onRunAll();
|
||||||
|
} else {
|
||||||
|
// Fallback: run each per-command callback in order
|
||||||
|
hits.forEach((_, idx) => {
|
||||||
|
try { onRunOneIdx?.(idx); } catch (e) {
|
||||||
|
RC_DEBUG?.warn('Run-all fallback failed for index', { idx, error: String(e) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
RC_DEBUG?.warn('Run-all handler failed', { error: String(e) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
bar.appendChild(runAllBtn);
|
||||||
|
|
||||||
|
hits.forEach((_, idx) => {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.textContent = `Run again [#${idx + 1}]`;
|
||||||
|
btn.style.cssText = 'padding:4px 10px; border:1px solid #374151; border-radius:4px; background:#1f2937; color:#e5e7eb;';
|
||||||
|
btn.addEventListener('click', (ev) => {
|
||||||
|
RC_DEBUG?.flashBtn?.(ev.currentTarget, 'Running');
|
||||||
|
try { onRunOneIdx(idx); } catch (e) {
|
||||||
|
RC_DEBUG?.warn('Run-again handler failed', { error: String(e) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
bar.appendChild(btn);
|
||||||
|
});
|
||||||
|
|
||||||
const dismiss = document.createElement('button');
|
const dismiss = document.createElement('button');
|
||||||
dismiss.textContent = 'Dismiss';
|
dismiss.textContent = 'Dismiss';
|
||||||
dismiss.style.cssText = 'padding:4px 10px; border:1px solid #374151; border-radius:4px; background:#111827; color:#9ca3af;';
|
dismiss.style.cssText = 'padding:4px 10px; border:1px solid #374151; border-radius:4px; background:#111827; color:#9ca3af;';
|
||||||
run.onclick = (ev) => { RC_DEBUG?.flashBtn?.(ev.currentTarget, 'Running'); onRun(); };
|
dismiss.addEventListener('click', (ev) => { RC_DEBUG?.flashBtn?.(ev.currentTarget, 'Dismissed'); bar.remove(); });
|
||||||
dismiss.onclick = (ev) => { RC_DEBUG?.flashBtn?.(ev.currentTarget, 'Dismissed'); bar.remove(); };
|
bar.appendChild(dismiss);
|
||||||
bar.append(msg, run, dismiss);
|
|
||||||
containerEl.appendChild(bar);
|
containerEl.appendChild(bar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Back-compat thin wrapper used elsewhere; now renders per-command for whatever is currently in the message.
|
||||||
|
function attachRunAgainUI(containerEl, onRunAllLegacy) {
|
||||||
|
const hitsNow = findAllCommandsInMessage(containerEl).slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE);
|
||||||
|
attachRunAgainPerCommand(containerEl, hitsNow, (idx) => {
|
||||||
|
// Preserve legacy behavior if a caller passed a single callback:
|
||||||
|
// default to re-enqueue just the selected index.
|
||||||
|
const h = hitsNow[idx];
|
||||||
|
if (!h) return;
|
||||||
|
commandMonitor.enqueueCommand(containerEl, h, idx);
|
||||||
|
}, () => {
|
||||||
|
// Legacy "run all" behavior for old callers
|
||||||
|
if (typeof onRunAllLegacy === 'function') {
|
||||||
|
onRunAllLegacy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hitsNow.forEach((h, i) => commandMonitor.enqueueCommand(containerEl, h, i));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// When resuming from pause, treat like a cold start & mark all currently-visible commands as processed.
|
// When resuming from pause, treat like a cold start & mark all currently-visible commands as processed.
|
||||||
// Adds "Run again" buttons so nothing auto-executes.
|
// Adds "Run again" buttons so nothing auto-executes.
|
||||||
function markExistingHitsAsProcessed() {
|
function markExistingHitsAsProcessed() {
|
||||||
|
|
@ -1104,7 +1163,11 @@
|
||||||
commandMonitor?.history?.markElement?.(el, idx + 1);
|
commandMonitor?.history?.markElement?.(el, idx + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
attachRunAgainUI(el, () => {
|
attachRunAgainPerCommand(el, capped, (idx) => {
|
||||||
|
const nowHits = findAllCommandsInMessage(el).slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE);
|
||||||
|
const h = nowHits[idx];
|
||||||
|
if (h) commandMonitor.enqueueCommand(el, h, idx);
|
||||||
|
}, () => {
|
||||||
const nowHits = findAllCommandsInMessage(el).slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE);
|
const nowHits = findAllCommandsInMessage(el).slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE);
|
||||||
nowHits.forEach((h, i) => commandMonitor.enqueueCommand(el, h, i));
|
nowHits.forEach((h, i) => commandMonitor.enqueueCommand(el, h, i));
|
||||||
});
|
});
|
||||||
|
|
@ -1215,7 +1278,8 @@
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findSendButton() {
|
function findSendButton(scopeEl) {
|
||||||
|
const scope = scopeEl?.closest('form, [data-testid="composer"], main') || document;
|
||||||
const selectors = [
|
const selectors = [
|
||||||
'button[data-testid="send-button"]',
|
'button[data-testid="send-button"]',
|
||||||
'button[aria-label*="Send"]',
|
'button[aria-label*="Send"]',
|
||||||
|
|
@ -2119,20 +2183,18 @@
|
||||||
if (withinColdStart || alreadyAll) {
|
if (withinColdStart || alreadyAll) {
|
||||||
el.dataset.aiRcProcessed = '1';
|
el.dataset.aiRcProcessed = '1';
|
||||||
|
|
||||||
RC_DEBUG?.verbose('Skipping command(s) - ' + (withinColdStart ? 'page load (cold start)' : 'already executed in this conversation'), {
|
RC_DEBUG?.verbose(
|
||||||
fingerprint: fingerprintElement(el).slice(0, 40) + '...',
|
'Skipping command(s) - ' +
|
||||||
commandCount: hits.length
|
(withinColdStart ? 'page load (cold start)' : 'already executed in this conversation'),
|
||||||
});
|
{ fingerprint: fingerprintElement(el).slice(0, 40) + '...', commandCount: hits.length }
|
||||||
|
);
|
||||||
|
|
||||||
attachRunAgainUI(el, () => {
|
attachRunAgainPerCommand(el, hits.slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE), (idx) => {
|
||||||
el.dataset.aiRcProcessed = '1';
|
el.dataset.aiRcProcessed = '1';
|
||||||
const hit2 = findAllCommandsInMessage(el);
|
const hit2 = findAllCommandsInMessage(el).slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE);
|
||||||
if (hit2.length) {
|
const h = hit2[idx];
|
||||||
const capped = hit2.slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE);
|
if (h) this.enqueueCommand(el, h, idx);
|
||||||
capped.forEach((h, i) => this.enqueueCommand(el, h, i));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
skipped += hits.length;
|
skipped += hits.length;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2273,23 +2335,29 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
attachRetryUI(element, messageId) {
|
attachRetryUI(element, messageId) {
|
||||||
if (element.querySelector('.ai-rc-rerun')) return;
|
const all = findAllCommandsInMessage(element).slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE);
|
||||||
|
if (!all.length) return;
|
||||||
|
|
||||||
attachRunAgainUI(element, () => {
|
// Parse failing index
|
||||||
|
const m = /#(\d+)$/.exec(messageId);
|
||||||
|
const failedIdx = m ? Math.max(0, parseInt(m[1], 10) - 1) : 0;
|
||||||
|
|
||||||
|
attachRunAgainPerCommand(element, all, (idx) => {
|
||||||
element.dataset.aiRcProcessed = '1';
|
element.dataset.aiRcProcessed = '1';
|
||||||
|
const pick = all[idx]?.text;
|
||||||
// Parse sub-index from messageId like "...#3"
|
if (!pick) return;
|
||||||
const m = /#(\d+)$/.exec(messageId);
|
this.trackedMessages.delete(messageId);
|
||||||
const wantIdx = m ? Math.max(0, parseInt(m[1], 10) - 1) : 0;
|
const newId = this.getReadableMessageId(element);
|
||||||
|
this.trackMessage(element, pick, newId);
|
||||||
const all = findAllCommandsInMessage(element);
|
|
||||||
const selected = all[wantIdx]?.text || all[0]?.text;
|
|
||||||
if (selected) {
|
|
||||||
this.trackedMessages.delete(messageId);
|
|
||||||
const newId = this.getReadableMessageId(element);
|
|
||||||
this.trackMessage(element, selected, newId);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Highlight failed one
|
||||||
|
try {
|
||||||
|
const bar = element.querySelector('.ai-rc-rerun');
|
||||||
|
const btns = [...bar.querySelectorAll('button')].filter(b => /Run again \[#\d+\]/.test(b.textContent || ''));
|
||||||
|
const b = btns[failedIdx];
|
||||||
|
if (b) b.style.outline = '2px solid #ef4444';
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue