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

Resume-safe behavior (no accidental re-runs after unpausing; Run again buttons instead).

Simple example: true opt-out that silently skips execution.

Better paste fidelity for triple-backtick blocks in the composer.

Helpful timing comments for future tuning.

Version set to 1.6.2.
This commit is contained in:
rob 2025-10-10 02:23:09 +00:00
parent e7fa36714f
commit 69faad29c0
1 changed files with 76 additions and 6 deletions

View File

@ -1,7 +1,7 @@
// ==UserScript==
// @name AI Repo Commander
// @namespace http://tampermonkey.net/
// @version 1.6.0
// @version 1.6.2
// @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
// @match https://chat.openai.com/*
@ -34,9 +34,10 @@
DEBUG_SHOW_PANEL: true,
// Timing & API
DEBOUNCE_DELAY: 3000,
// If you see "debouncing → error" in logs (assistant streams very slowly),
// try bumping DEBOUNCE_DELAY by +10002000 and/or SETTLE_CHECK_MS by +400800. DEBOUNCE_DELAY: 3000,
MAX_RETRIES: 2,
VERSION: '1.6.0',
VERSION: '1.6.2',
API_TIMEOUT_MS: 60000,
PROCESS_EXISTING: false,
@ -60,7 +61,8 @@
SUBMIT_MODE: 'button_first',
// Streaming-complete hardening
REQUIRE_TERMINATOR: true,
// 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,
@ -414,16 +416,27 @@
const pauseBtn = root.querySelector('.rc-pause');
pauseBtn.addEventListener('click', () => {
const wasPaused = this.cfg.RUNTIME.PAUSED;
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' : '';
this.flashBtn(pauseBtn, this.cfg.RUNTIME.PAUSED ? 'Paused' : 'Resumed');
this.toast(this.cfg.RUNTIME.PAUSED ? 'Paused scanning' : 'Resumed scanning');
this.info(`Runtime ${this.cfg.RUNTIME.PAUSED ? 'paused' : 'resumed'}`);
// When RESUMING: start a short cold-start window and mark current hits as processed.
if (wasPaused && !this.cfg.RUNTIME.PAUSED) {
if (commandMonitor) {
commandMonitor.coldStartUntil = Date.now() + (CONFIG.COLD_START_MS || 2000);
}
markExistingHitsAsProcessed();
}
});
// Queue clear button
const queueBtn = root.querySelector('.rc-queue-clear');
queueBtn.addEventListener('click', (e) => {
@ -1062,6 +1075,33 @@
containerEl.appendChild(bar);
}
// 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() {
try {
const messages = document.querySelectorAll(MSG_SELECTORS.join(','));
messages.forEach((el) => {
const hits = findAllCommandsInMessage(el);
if (!hits.length) return;
el.dataset.aiRcProcessed = '1';
const capped = hits.slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE);
capped.forEach((_, idx) => {
commandMonitor?.history?.markElement?.(el, idx + 1);
});
attachRunAgainUI(el, () => {
const nowHits = findAllCommandsInMessage(el).slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE);
nowHits.forEach((h, i) => commandMonitor.enqueueCommand(el, h, i));
});
});
RC_DEBUG?.info('Resume-safe guard: marked visible commands as processed & attached Run again buttons');
} catch (e) {
RC_DEBUG?.warn('Resume-safe guard failed', { error: String(e) });
}
}
// ---------------------- UI feedback ----------------------
class UIFeedback {
static appendStatus(sourceElement, templateType, data) {
@ -1197,8 +1237,16 @@
const isPM = el.classList && el.classList.contains('ProseMirror');
if (isPM) {
RC_DEBUG?.verbose('Attempting ProseMirror paste');
// Pad with blank lines before/after to preserve ``` fences visually.
const payload2 = `\n${payload.replace(/\n?$/, '\n')}\n`;
const escape = (s) => s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
const html = String(payload).split('\n').map(line => line.length ? `<p>${escape(line)}</p>` : '<p><br></p>').join('');
const html = String(payload2)
.split('\n')
.map(line => line.length ? `<p>${escape(line)}</p>` : '<p><br></p>')
.join('');
el.innerHTML = html;
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
@ -1374,17 +1422,31 @@
static validateStructure(parsed) {
const errors = [];
// Example commands are treated as valid but inert
const isExample =
parsed.example === true ||
parsed.example === 'true' ||
String(parsed.example || '').toLowerCase() === 'yes';
const action = parsed.action;
if (isExample) {
return { isValid: true, errors, example: true };
}
if (!action) { errors.push('Missing required field: action'); return { isValid:false, errors }; }
const req = REQUIRED_FIELDS[action];
if (!req) { errors.push(`Unknown action: ${action}`); return { isValid:false, errors }; }
for (const f of req) if (!parsed[f] && parsed[f] !== '') errors.push(`Missing required field: ${f}`);
for (const [field, value] of Object.entries(parsed)) {
const validator = FIELD_VALIDATORS[field];
if (validator && !validator(value)) errors.push(`Invalid format for field: ${field}`);
}
return { isValid: errors.length === 0, errors };
}
}
// ---------------------- Execution ----------------------
@ -1858,7 +1920,7 @@
this.trackMessage(el, hits[0].text, this.getReadableMessageId(el));
return;
}
const withinColdStart = Date.now() < this.coldStartUntil;
const alreadyAll = hits.every((_, i) => this.history.hasElement(el, i + 1));
@ -2103,6 +2165,14 @@
// 2) Validate
this.updateState(messageId, COMMAND_STATES.VALIDATING);
let validation = CommandParser.validateStructure(parsed);
// Silently skip examples (already marked in history by the scanner)
if (validation.example) {
RC_DEBUG?.info('Example command detected — skipping execution');
this.updateState(messageId, COMMAND_STATES.COMPLETE);
return;
}
if (!validation.isValid) {
this.attachRetryUI(message.element, messageId);
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);