fixed all warnings and the docs

This commit is contained in:
rob 2025-10-15 11:20:01 -03:00
parent e2e970b9d9
commit 7a441aee48
1 changed files with 268 additions and 37 deletions

View File

@ -63,8 +63,10 @@
// Paste + submit behavior
APPEND_TRAILING_NEWLINE: true,
AUTO_SUBMIT: true,
POST_PASTE_DELAY_MS: 250,
POST_PASTE_DELAY_MS: 600,
SUBMIT_MODE: 'button_first',
MAX_COMPOSER_WAIT_MS: 15 * 60 * 1000, // 15 minutes
SUBMIT_MAX_RETRIES: 12,
// Streaming-complete hardening
// SETTLE_CHECK_MS is the "stable window" after last text change;
@ -81,12 +83,14 @@
SCAN_DEBOUNCE_MS: 400,
FAST_WARN_MS: 50,
SLOW_WARN_MS: 60_000,
CLUSTER_RESCAN_MS: 1000, // time window to rescan adjacent messages
CLUSTER_MAX_LOOKAHEAD: 3, // how many adjacent assistant messages to check
// Queue management
QUEUE_MIN_DELAY_MS: 800,
QUEUE_MIN_DELAY_MS: 1500,
QUEUE_MAX_PER_MINUTE: 15,
QUEUE_MAX_PER_MESSAGE: 5,
QUEUE_WAIT_FOR_COMPOSER_MS: 6000,
QUEUE_WAIT_FOR_COMPOSER_MS: 12000,
RESPONSE_BUFFER_FLUSH_DELAY_MS: 500, // wait for siblings to finish
RESPONSE_BUFFER_SECTION_HEADINGS: true,
@ -112,6 +116,11 @@
const CONFIG = loadSavedConfig();
// Ensure response buffer singleton exists before command execution
if (!window.AI_REPO_RESPONSES) {
window.AI_REPO_RESPONSES = new ResponseBuffer();
}
// ---------------------- Debug Console ----------------------
let RC_DEBUG = null;
@ -826,6 +835,90 @@
return hits;
}
// Chainable actions that may trigger cluster rescan
const chainableActions = ['create_repo', 'create_file', 'create_branch', 'update_file', 'delete_file', 'create_pr'];
// 1) Check if we should trigger a cluster rescan after executing an action
function shouldTriggerClusterRescan(anchorEl, justExecutedAction) {
if (!chainableActions.includes(justExecutedAction)) return false;
// Check if next sibling is an unprocessed assistant message
let nextSibling = anchorEl?.nextElementSibling;
while (nextSibling) {
// Stop at user messages
if (commandMonitor && !commandMonitor.isAssistantMessage(nextSibling)) return false;
// Check if it's an assistant message
if (commandMonitor && commandMonitor.isAssistantMessage(nextSibling)) {
// Check if unprocessed (no processed marker)
const hasMarker = nextSibling?.dataset?.aiRcProcessed === '1' || !!nextSibling.querySelector('.ai-rc-queue-badge');
return !hasMarker;
}
nextSibling = nextSibling.nextElementSibling;
}
return false;
}
// 2) Schedule a cluster rescan to check adjacent assistant messages
async function scheduleClusterRescan(anchorEl) {
if (!anchorEl) return;
RC_DEBUG?.info('Scheduling cluster rescan', { anchor: anchorEl });
const deadline = Date.now() + CONFIG.CLUSTER_RESCAN_MS;
let scanned = 0;
let currentEl = anchorEl.nextElementSibling;
while (currentEl && scanned < CONFIG.CLUSTER_MAX_LOOKAHEAD && Date.now() < deadline) {
// Stop at user message boundaries
if (commandMonitor && !commandMonitor.isAssistantMessage(currentEl)) {
RC_DEBUG?.verbose('Cluster rescan hit user message boundary');
break;
}
// Only process assistant messages
if (commandMonitor && commandMonitor.isAssistantMessage(currentEl)) {
// Check if already processed
const hasMarker = currentEl?.dataset?.aiRcProcessed === '1' || !!currentEl.querySelector('.ai-rc-queue-badge');
if (!hasMarker) {
// Look for new @bridge@ blocks
const hits = findAllCommandsInMessage(currentEl);
if (hits.length > 0) {
RC_DEBUG?.info('Cluster rescan found commands in adjacent message', { count: hits.length });
// 1) Set dataset marker
currentEl.dataset.aiRcProcessed = '1';
// 2) Slice hits to CONFIG.QUEUE_MAX_PER_MESSAGE
const capped = hits.slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE);
// 3) Mark and enqueue each command
capped.forEach((h, idx) => {
if (commandMonitor) {
commandMonitor.history.markElement(currentEl, idx + 1);
commandMonitor.enqueueCommand(currentEl, h, idx);
}
});
// 4) Add queue badge with capped count
attachQueueBadge(currentEl, capped.length);
}
}
scanned++;
}
currentEl = currentEl.nextElementSibling;
// Small delay between checks
if (currentEl && Date.now() < deadline) {
await ExecutionManager.delay(100);
}
}
RC_DEBUG?.verbose('Cluster rescan completed', { scanned, deadline: Date.now() >= deadline });
}
// Tiny badge on the message showing how many got queued
function attachQueueBadge(el, count) {
if (el.querySelector('.ai-rc-queue-badge')) return;
@ -851,10 +944,28 @@
}
const el = getVisibleInputCandidate();
const btn = findSendButton(el);
const btnReady = !btn || (!btn.disabled && btn.getAttribute('aria-disabled') !== 'true');
const btnReady = CONFIG.SUBMIT_MODE === 'enter_only'
? true
: (!!btn && !btn.disabled && btn.getAttribute('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;
// 1) Add typing indicator to busy selector
const busy = scope.querySelector('[aria-busy="true"], [data-state="loading"], .typing-indicator');
// 2) Check if composer has unsent content
let hasUnsent = false;
if (el) {
try {
const currentText = (el.textContent || el.value || '').trim();
if (currentText.startsWith('@bridge@') || currentText.startsWith('### [')) {
hasUnsent = true;
}
} catch (e) {
RC_DEBUG?.verbose('Failed to check composer content', { error: String(e) });
}
}
if (el && btnReady && !busy && !hasUnsent) return true;
await ExecutionManager.delay(pollMs);
}
RC_DEBUG?.warn('Composer not ready within timeout');
@ -1329,10 +1440,19 @@
async function submitComposer() {
try {
const btn = findSendButton();
if (CONFIG.SUBMIT_MODE !== 'enter_only' && btn) { btn.click(); return true; }
// 1) Get composer element first
const el = getVisibleInputCandidate();
if (!el) return false;
// 2) Find send button scoped to composer
const btn = findSendButton(el);
// 3) Check SUBMIT_MODE and click or press Enter
if (CONFIG.SUBMIT_MODE !== 'enter_only' && btn) {
btn.click();
return true;
}
return pressEnterOn(el);
} catch {
return false;
@ -1384,12 +1504,11 @@
// 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;');
el.innerHTML = String(payload2)
.split('\n')
.map(line => line.length ? `<p>${escape(line)}</p>` : '<p><br></p>')
.join('');
// Use text node to preserve code fences better
const textNode = document.createTextNode(payload2);
el.innerHTML = '';
el.appendChild(textNode);
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
RC_DEBUG?.info('✅ Paste method succeeded: ProseMirror');
@ -1478,34 +1597,90 @@
}
}
async function pasteAndMaybeSubmit(text) {
const ready = await waitForComposerReady({ timeoutMs: CONFIG.QUEUE_WAIT_FOR_COMPOSER_MS });
if (!ready) {
RC_DEBUG?.warn('Composer not ready; re-queueing paste');
execQueue.push(async () => { await pasteAndMaybeSubmit(text); });
async function pasteAndMaybeSubmit(text, attempt = 0, startedAt = Date.now(), submitRetry = 0) {
// 1) Check if elapsed time exceeds MAX_COMPOSER_WAIT_MS
const elapsed = Date.now() - startedAt;
if (elapsed > CONFIG.MAX_COMPOSER_WAIT_MS) {
RC_DEBUG?.error('pasteAndMaybeSubmit gave up after max wait time', {
elapsed,
maxWait: CONFIG.MAX_COMPOSER_WAIT_MS,
attempt,
submitRetry
});
GM_notification({
title: 'AI Repo Commander',
text: `Paste/submit failed: composer not ready after ${Math.floor(elapsed / 1000)}s`,
timeout: 6000
});
return false;
}
const pasted = pasteToComposer(text);
if (!pasted) return false;
try {
const el = getVisibleInputCandidate();
const actualContent = el?.textContent || el?.value || '[no content found]';
RC_DEBUG?.info('📋 Content in composer after paste', {
expectedLength: text.length,
actualLength: actualContent.length,
actualPreview: actualContent.substring(0, 200)
// 2) Quick readiness probe with 1200ms timeout
const ready = await waitForComposerReady({ timeoutMs: 1200 });
if (!ready) {
// 3) Not ready, requeue with exponential backoff (600ms base, cap at 30s)
const backoffMs = Math.min(30_000, Math.floor(600 * Math.pow(1.6, attempt)));
RC_DEBUG?.warn('Composer not ready; re-queueing paste with backoff', {
attempt,
backoffMs,
elapsed
});
} catch (e) {
RC_DEBUG?.warn('Could not read composer content', { error: String(e) });
setTimeout(() => {
execQueue.push(async () => {
await pasteAndMaybeSubmit(text, attempt + 1, startedAt, submitRetry);
});
}, backoffMs);
return false;
}
// 4) Only paste if text is non-empty (enables submit-only retries)
if (text && text.length > 0) {
const pasted = pasteToComposer(text);
if (!pasted) return false;
try {
const el = getVisibleInputCandidate();
const actualContent = el?.textContent || el?.value || '[no content found]';
RC_DEBUG?.info('📋 Content in composer after paste', {
expectedLength: text.length,
actualLength: actualContent.length,
actualPreview: actualContent.substring(0, 200)
});
} catch (e) {
RC_DEBUG?.warn('Could not read composer content', { error: String(e) });
}
}
if (!CONFIG.AUTO_SUBMIT) return true;
// 5) After paste, wait POST_PASTE_DELAY_MS
await ExecutionManager.delay(CONFIG.POST_PASTE_DELAY_MS);
// 6) Try submitComposer()
const ok = await submitComposer();
if (!ok) {
GM_notification({ title: 'AI Repo Commander', text: 'Pasted content, but auto-submit did not trigger.', timeout: 4000 });
// 7) If submit fails, and we haven't hit SUBMIT_MAX_RETRIES, requeue submit-only retry
if (submitRetry < CONFIG.SUBMIT_MAX_RETRIES) {
const submitBackoffMs = Math.min(30_000, Math.floor(500 * Math.pow(1.6, submitRetry)));
RC_DEBUG?.warn('Submit failed; re-queueing submit-only retry with backoff', {
submitRetry,
submitBackoffMs
});
setTimeout(() => {
execQueue.push(async () => {
// Empty text for submit-only retry, increment submitRetry
await pasteAndMaybeSubmit('', attempt, startedAt, submitRetry + 1);
});
}, submitBackoffMs);
return false;
} else {
RC_DEBUG?.error('Submit failed after max retries', { submitRetry, maxRetries: CONFIG.SUBMIT_MAX_RETRIES });
GM_notification({
title: 'AI Repo Commander',
text: `Pasted content, but auto-submit failed after ${CONFIG.SUBMIT_MAX_RETRIES} retries.`,
timeout: 6000
});
}
}
return true;
}
@ -1743,7 +1918,7 @@
if (command.action === 'get_file') {
const body = this._extractGetFileBody(data);
if (typeof body === 'string' && body.length) {
new ResponseBuffer().push({ label, content: body });
(window.AI_REPO_RESPONSES || new ResponseBuffer()).push({ label, content: body });
} else {
GM_notification({ title: 'AI Repo Commander', text: 'get_file succeeded, but no content to paste.', timeout: 4000 });
}
@ -1753,10 +1928,10 @@
const files = this._extractFilesArray(data);
if (files && files.length) {
const listing = this._formatFilesListing(files);
new ResponseBuffer().push({ label, content: listing });
(window.AI_REPO_RESPONSES || new ResponseBuffer()).push({ label, content: listing });
} else {
const fallback = '```json\n' + JSON.stringify(data, null, 2) + '\n```';
new ResponseBuffer().push({ label, content: fallback });
(window.AI_REPO_RESPONSES || new ResponseBuffer()).push({ label, content: fallback });
GM_notification({
title: 'AI Repo Commander',
text: 'list_files succeeded, but response had no obvious files array. Pasted raw JSON.',
@ -1765,6 +1940,15 @@
}
}
// Trigger cluster rescan for chainable commands
try {
if (shouldTriggerClusterRescan(sourceElement, command.action)) {
await scheduleClusterRescan(sourceElement);
}
} catch (e) {
RC_DEBUG?.verbose('Cluster rescan failed', { error: String(e) });
}
return { success: true, data, isMock };
}
@ -2070,6 +2254,7 @@
// MutationObserver for immediate detection - watching edits AND additions
this.observer = new MutationObserver((mutations) => {
let shouldScan = false;
let adjacentToProcessed = false;
let reasons = new Set();
for (const m of mutations) {
@ -2115,7 +2300,33 @@
if (shouldScan) break;
}
if (shouldScan) {
// D) Check for new assistant messages adjacent to already-processed ones (split messages)
if (!adjacentToProcessed) {
for (const m of mutations) {
if (m.type === 'childList') {
for (const node of m.addedNodes) {
if (node.nodeType !== 1) continue;
// Check if it's an assistant message
const isAssistantMsg = node.matches?.(this.currentPlatform.messages) &&
this.isAssistantMessage(node);
if (isAssistantMsg) {
// Check if previous sibling is a processed assistant message
const prev = node.previousElementSibling;
if (prev && prev.dataset?.aiRcProcessed === '1' && this.isAssistantMessage(prev)) {
reasons.add('split message detected');
adjacentToProcessed = true;
break;
}
}
}
if (adjacentToProcessed) break;
}
}
}
if (shouldScan || adjacentToProcessed) {
RC_DEBUG?.trace('MO: scan triggered', { reasons: Array.from(reasons).join(', ') });
scheduleScan();
}
@ -2256,7 +2467,27 @@
const subId = `${messageId}#${idx + 1}`;
execQueue.push(async () => {
const finalTxt = hit.text; // <<< ADD THIS
// Micro-settle: wait for text to stabilize before parsing
try {
const blockElement = hit.blockElement;
if (blockElement) {
let lastText = blockElement.textContent || '';
const maxWait = 400;
const checkInterval = 80;
const startTime = Date.now();
while (Date.now() - startTime < maxWait) {
await new Promise(r => setTimeout(r, checkInterval));
const currentText = blockElement.textContent || '';
if (currentText === lastText) break; // Text stabilized
lastText = currentText;
}
}
} catch (e) {
RC_DEBUG?.verbose('Micro-settle failed, continuing anyway', { error: String(e) });
}
const finalTxt = hit.text;
let parsed;
try {
parsed = CommandParser.parseYAMLCommand(finalTxt);