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:
parent
e7fa36714f
commit
69faad29c0
|
|
@ -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 +1000–2000 and/or SETTLE_CHECK_MS by +400–800. 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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
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(', ')}`);
|
||||
|
|
|
|||
Loading…
Reference in New Issue