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==
|
// ==UserScript==
|
||||||
// @name AI Repo Commander
|
// @name AI Repo Commander
|
||||||
// @namespace http://tampermonkey.net/
|
// @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
|
// @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/*
|
||||||
|
|
@ -34,9 +34,10 @@
|
||||||
DEBUG_SHOW_PANEL: true,
|
DEBUG_SHOW_PANEL: true,
|
||||||
|
|
||||||
// Timing & API
|
// 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,
|
MAX_RETRIES: 2,
|
||||||
VERSION: '1.6.0',
|
VERSION: '1.6.2',
|
||||||
API_TIMEOUT_MS: 60000,
|
API_TIMEOUT_MS: 60000,
|
||||||
|
|
||||||
PROCESS_EXISTING: false,
|
PROCESS_EXISTING: false,
|
||||||
|
|
@ -60,7 +61,8 @@
|
||||||
SUBMIT_MODE: 'button_first',
|
SUBMIT_MODE: 'button_first',
|
||||||
|
|
||||||
// Streaming-complete hardening
|
// 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_CHECK_MS: 800,
|
||||||
SETTLE_POLL_MS: 200,
|
SETTLE_POLL_MS: 200,
|
||||||
|
|
||||||
|
|
@ -414,16 +416,27 @@
|
||||||
|
|
||||||
const pauseBtn = root.querySelector('.rc-pause');
|
const pauseBtn = root.querySelector('.rc-pause');
|
||||||
pauseBtn.addEventListener('click', () => {
|
pauseBtn.addEventListener('click', () => {
|
||||||
|
const wasPaused = this.cfg.RUNTIME.PAUSED;
|
||||||
this.cfg.RUNTIME.PAUSED = !this.cfg.RUNTIME.PAUSED;
|
this.cfg.RUNTIME.PAUSED = !this.cfg.RUNTIME.PAUSED;
|
||||||
saveConfig(this.cfg);
|
saveConfig(this.cfg);
|
||||||
|
|
||||||
pauseBtn.textContent = this.cfg.RUNTIME.PAUSED ? 'Resume' : 'Pause';
|
pauseBtn.textContent = this.cfg.RUNTIME.PAUSED ? 'Resume' : 'Pause';
|
||||||
pauseBtn.style.background = this.cfg.RUNTIME.PAUSED ? '#f59e0b' : '';
|
pauseBtn.style.background = this.cfg.RUNTIME.PAUSED ? '#f59e0b' : '';
|
||||||
pauseBtn.style.color = this.cfg.RUNTIME.PAUSED ? '#111827' : '';
|
pauseBtn.style.color = this.cfg.RUNTIME.PAUSED ? '#111827' : '';
|
||||||
this.flashBtn(pauseBtn, this.cfg.RUNTIME.PAUSED ? 'Paused' : 'Resumed');
|
this.flashBtn(pauseBtn, this.cfg.RUNTIME.PAUSED ? 'Paused' : 'Resumed');
|
||||||
this.toast(this.cfg.RUNTIME.PAUSED ? 'Paused scanning' : 'Resumed scanning');
|
this.toast(this.cfg.RUNTIME.PAUSED ? 'Paused scanning' : 'Resumed scanning');
|
||||||
this.info(`Runtime ${this.cfg.RUNTIME.PAUSED ? 'paused' : 'resumed'}`);
|
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
|
// Queue clear button
|
||||||
const queueBtn = root.querySelector('.rc-queue-clear');
|
const queueBtn = root.querySelector('.rc-queue-clear');
|
||||||
queueBtn.addEventListener('click', (e) => {
|
queueBtn.addEventListener('click', (e) => {
|
||||||
|
|
@ -1062,6 +1075,33 @@
|
||||||
containerEl.appendChild(bar);
|
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 ----------------------
|
// ---------------------- UI feedback ----------------------
|
||||||
class UIFeedback {
|
class UIFeedback {
|
||||||
static appendStatus(sourceElement, templateType, data) {
|
static appendStatus(sourceElement, templateType, data) {
|
||||||
|
|
@ -1197,8 +1237,16 @@
|
||||||
const isPM = el.classList && el.classList.contains('ProseMirror');
|
const isPM = el.classList && el.classList.contains('ProseMirror');
|
||||||
if (isPM) {
|
if (isPM) {
|
||||||
RC_DEBUG?.verbose('Attempting ProseMirror paste');
|
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 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.innerHTML = html;
|
||||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
|
@ -1374,17 +1422,31 @@
|
||||||
|
|
||||||
static validateStructure(parsed) {
|
static validateStructure(parsed) {
|
||||||
const errors = [];
|
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;
|
const action = parsed.action;
|
||||||
|
|
||||||
|
if (isExample) {
|
||||||
|
return { isValid: true, errors, example: true };
|
||||||
|
}
|
||||||
|
|
||||||
if (!action) { errors.push('Missing required field: action'); return { isValid:false, errors }; }
|
if (!action) { errors.push('Missing required field: action'); return { isValid:false, errors }; }
|
||||||
const req = REQUIRED_FIELDS[action];
|
const req = REQUIRED_FIELDS[action];
|
||||||
if (!req) { errors.push(`Unknown action: ${action}`); return { isValid:false, errors }; }
|
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 f of req) if (!parsed[f] && parsed[f] !== '') errors.push(`Missing required field: ${f}`);
|
||||||
|
|
||||||
for (const [field, value] of Object.entries(parsed)) {
|
for (const [field, value] of Object.entries(parsed)) {
|
||||||
const validator = FIELD_VALIDATORS[field];
|
const validator = FIELD_VALIDATORS[field];
|
||||||
if (validator && !validator(value)) errors.push(`Invalid format for field: ${field}`);
|
if (validator && !validator(value)) errors.push(`Invalid format for field: ${field}`);
|
||||||
}
|
}
|
||||||
return { isValid: errors.length === 0, errors };
|
return { isValid: errors.length === 0, errors };
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------- Execution ----------------------
|
// ---------------------- Execution ----------------------
|
||||||
|
|
@ -1858,7 +1920,7 @@
|
||||||
this.trackMessage(el, hits[0].text, this.getReadableMessageId(el));
|
this.trackMessage(el, hits[0].text, this.getReadableMessageId(el));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const withinColdStart = Date.now() < this.coldStartUntil;
|
const withinColdStart = Date.now() < this.coldStartUntil;
|
||||||
const alreadyAll = hits.every((_, i) => this.history.hasElement(el, i + 1));
|
const alreadyAll = hits.every((_, i) => this.history.hasElement(el, i + 1));
|
||||||
|
|
||||||
|
|
@ -2103,6 +2165,14 @@
|
||||||
// 2) Validate
|
// 2) Validate
|
||||||
this.updateState(messageId, COMMAND_STATES.VALIDATING);
|
this.updateState(messageId, COMMAND_STATES.VALIDATING);
|
||||||
let validation = CommandParser.validateStructure(parsed);
|
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) {
|
if (!validation.isValid) {
|
||||||
this.attachRetryUI(message.element, messageId);
|
this.attachRetryUI(message.element, messageId);
|
||||||
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
|
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue