diff --git a/src/ai-repo-commander.user.js b/src/ai-repo-commander.user.js index 83d87b5..79365f3 100644 --- a/src/ai-repo-commander.user.js +++ b/src/ai-repo-commander.user.js @@ -12,6 +12,7 @@ // @grant GM_notification // @grant GM_setClipboard // @connect n8n.brrd.tech +// @connect * // ==/UserScript== (function () { @@ -37,7 +38,7 @@ // 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. - DEBOUNCE_DELAY: 3000, + DEBOUNCE_DELAY: 6500, MAX_RETRIES: 2, VERSION: '1.6.2', API_TIMEOUT_MS: 60000, @@ -66,15 +67,15 @@ // SETTLE_CHECK_MS is the "stable window" after last text change; // SETTLE_POLL_MS is how often we re-check the code block. REQUIRE_TERMINATOR: true, - SETTLE_CHECK_MS: 800, - SETTLE_POLL_MS: 200, + SETTLE_CHECK_MS: 1300, + SETTLE_POLL_MS: 250, // Runtime toggles RUNTIME: { PAUSED: false }, // New additions for hardening STUCK_AFTER_MS: 10 * 60 * 1000, - SCAN_DEBOUNCE_MS: 250, + SCAN_DEBOUNCE_MS: 400, FAST_WARN_MS: 50, SLOW_WARN_MS: 60_000, @@ -507,7 +508,8 @@ // Dragging const header = root.querySelector('.rc-header'); 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; const rect = root.getBoundingClientRect(); this.drag.dx = e.clientX - rect.left; @@ -828,12 +830,10 @@ continue; } const el = getVisibleInputCandidate(); - const btn = findSendButton(); + const btn = findSendButton(el); const btnReady = !btn || (!btn.disabled && btn.getAttribute('aria-disabled') !== 'true'); - const busy = document.querySelector( - '[aria-busy="true"], [data-state="loading"], ' + - 'button[disabled], button[aria-disabled="true"]' - ); + const scope = el?.closest('form, [data-testid="composer"], main, body') || document; + const busy = scope.querySelector('[aria-busy="true"], [data-state="loading"]'); if (el && btnReady && !busy) return true; await ExecutionManager.delay(pollMs); } @@ -918,8 +918,8 @@ repo: (v) => /^[\w\-\.]+(\/[\w\-\.]+)?$/.test(v), path: (v) => !v.includes('..') && !v.startsWith('/') && !v.includes('\\'), action: (v) => Object.keys(REQUIRED_FIELDS).includes(v), - owner: (v) => !v || /^[\w\-]+$/.test(v), - url: (v) => !v || /^https?:\/\/.+\..+/.test(v), + url: (v) => !v || /^https?:\/\/[^/\s]+(?:\/|$)/i.test(v), + owner: (v) => !v || /^[\w\-.]+$/.test(v), branch: (v) => v && v.length > 0 && !v.includes('..'), source_branch:(v) => !v || (v.length > 0 && !v.includes('..')), head: (v) => v && v.length > 0, @@ -1068,26 +1068,85 @@ setConfig: (patch) => { Object.assign(CONFIG, patch||{}); saveConfig(CONFIG); }, }; - function attachRunAgainUI(containerEl, onRun) { - if (containerEl.querySelector('.ai-rc-rerun')) return; + // Replace the whole attachRunAgainUI with this per-command version (and keep a thin wrapper for back-compat) + 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'); 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'); - msg.textContent = 'Already executed.'; - msg.style.cssText = 'flex:1; font-size:13px; opacity:.9;'; - const run = document.createElement('button'); - run.textContent = 'Run again'; - run.style.cssText = 'padding:4px 10px; border:1px solid #374151; border-radius:4px; background:#1f2937; color:#e5e7eb;'; + msg.textContent = `Already executed. Re-run:`; + msg.style.cssText = 'font-size:13px; opacity:.9; margin-right:6px;'; + bar.appendChild(msg); + // "Run all again" button (optional legacy support) + 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'); dismiss.textContent = 'Dismiss'; 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.onclick = (ev) => { RC_DEBUG?.flashBtn?.(ev.currentTarget, 'Dismissed'); bar.remove(); }; - bar.append(msg, run, dismiss); + dismiss.addEventListener('click', (ev) => { RC_DEBUG?.flashBtn?.(ev.currentTarget, 'Dismissed'); bar.remove(); }); + bar.appendChild(dismiss); + 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. // Adds "Run again" buttons so nothing auto-executes. function markExistingHitsAsProcessed() { @@ -1104,7 +1163,11 @@ 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); nowHits.forEach((h, i) => commandMonitor.enqueueCommand(el, h, i)); }); @@ -1215,7 +1278,8 @@ return null; } - function findSendButton() { + function findSendButton(scopeEl) { + const scope = scopeEl?.closest('form, [data-testid="composer"], main') || document; const selectors = [ 'button[data-testid="send-button"]', 'button[aria-label*="Send"]', @@ -2119,20 +2183,18 @@ if (withinColdStart || alreadyAll) { el.dataset.aiRcProcessed = '1'; - RC_DEBUG?.verbose('Skipping command(s) - ' + (withinColdStart ? 'page load (cold start)' : 'already executed in this conversation'), { - fingerprint: fingerprintElement(el).slice(0, 40) + '...', - commandCount: hits.length - }); + RC_DEBUG?.verbose( + 'Skipping command(s) - ' + + (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'; - const hit2 = findAllCommandsInMessage(el); - if (hit2.length) { - const capped = hit2.slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE); - capped.forEach((h, i) => this.enqueueCommand(el, h, i)); - } + const hit2 = findAllCommandsInMessage(el).slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE); + const h = hit2[idx]; + if (h) this.enqueueCommand(el, h, idx); }); - skipped += hits.length; return; } @@ -2273,23 +2335,29 @@ } 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'; - - // 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); - const newId = this.getReadableMessageId(element); - this.trackMessage(element, selected, newId); - } + const pick = all[idx]?.text; + if (!pick) return; + this.trackedMessages.delete(messageId); + const newId = this.getReadableMessageId(element); + this.trackMessage(element, pick, 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 {} }