fixed all warnings and the docs
This commit is contained in:
parent
e2e970b9d9
commit
7a441aee48
|
|
@ -63,8 +63,10 @@
|
||||||
// Paste + submit behavior
|
// Paste + submit behavior
|
||||||
APPEND_TRAILING_NEWLINE: true,
|
APPEND_TRAILING_NEWLINE: true,
|
||||||
AUTO_SUBMIT: true,
|
AUTO_SUBMIT: true,
|
||||||
POST_PASTE_DELAY_MS: 250,
|
POST_PASTE_DELAY_MS: 600,
|
||||||
SUBMIT_MODE: 'button_first',
|
SUBMIT_MODE: 'button_first',
|
||||||
|
MAX_COMPOSER_WAIT_MS: 15 * 60 * 1000, // 15 minutes
|
||||||
|
SUBMIT_MAX_RETRIES: 12,
|
||||||
|
|
||||||
// Streaming-complete hardening
|
// Streaming-complete hardening
|
||||||
// SETTLE_CHECK_MS is the "stable window" after last text change;
|
// SETTLE_CHECK_MS is the "stable window" after last text change;
|
||||||
|
|
@ -81,12 +83,14 @@
|
||||||
SCAN_DEBOUNCE_MS: 400,
|
SCAN_DEBOUNCE_MS: 400,
|
||||||
FAST_WARN_MS: 50,
|
FAST_WARN_MS: 50,
|
||||||
SLOW_WARN_MS: 60_000,
|
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 management
|
||||||
QUEUE_MIN_DELAY_MS: 800,
|
QUEUE_MIN_DELAY_MS: 1500,
|
||||||
QUEUE_MAX_PER_MINUTE: 15,
|
QUEUE_MAX_PER_MINUTE: 15,
|
||||||
QUEUE_MAX_PER_MESSAGE: 5,
|
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_FLUSH_DELAY_MS: 500, // wait for siblings to finish
|
||||||
RESPONSE_BUFFER_SECTION_HEADINGS: true,
|
RESPONSE_BUFFER_SECTION_HEADINGS: true,
|
||||||
|
|
@ -112,6 +116,11 @@
|
||||||
|
|
||||||
const CONFIG = loadSavedConfig();
|
const CONFIG = loadSavedConfig();
|
||||||
|
|
||||||
|
// Ensure response buffer singleton exists before command execution
|
||||||
|
if (!window.AI_REPO_RESPONSES) {
|
||||||
|
window.AI_REPO_RESPONSES = new ResponseBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------- Debug Console ----------------------
|
// ---------------------- Debug Console ----------------------
|
||||||
let RC_DEBUG = null;
|
let RC_DEBUG = null;
|
||||||
|
|
||||||
|
|
@ -826,6 +835,90 @@
|
||||||
return hits;
|
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
|
// Tiny badge on the message showing how many got queued
|
||||||
function attachQueueBadge(el, count) {
|
function attachQueueBadge(el, count) {
|
||||||
if (el.querySelector('.ai-rc-queue-badge')) return;
|
if (el.querySelector('.ai-rc-queue-badge')) return;
|
||||||
|
|
@ -851,10 +944,28 @@
|
||||||
}
|
}
|
||||||
const el = getVisibleInputCandidate();
|
const el = getVisibleInputCandidate();
|
||||||
const btn = findSendButton(el);
|
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 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);
|
await ExecutionManager.delay(pollMs);
|
||||||
}
|
}
|
||||||
RC_DEBUG?.warn('Composer not ready within timeout');
|
RC_DEBUG?.warn('Composer not ready within timeout');
|
||||||
|
|
@ -1329,10 +1440,19 @@
|
||||||
|
|
||||||
async function submitComposer() {
|
async function submitComposer() {
|
||||||
try {
|
try {
|
||||||
const btn = findSendButton();
|
// 1) Get composer element first
|
||||||
if (CONFIG.SUBMIT_MODE !== 'enter_only' && btn) { btn.click(); return true; }
|
|
||||||
const el = getVisibleInputCandidate();
|
const el = getVisibleInputCandidate();
|
||||||
if (!el) return false;
|
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);
|
return pressEnterOn(el);
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -1384,12 +1504,11 @@
|
||||||
|
|
||||||
// Pad with blank lines before/after to preserve ``` fences visually.
|
// Pad with blank lines before/after to preserve ``` fences visually.
|
||||||
const payload2 = `\n${payload.replace(/\n?$/, '\n')}\n`;
|
const payload2 = `\n${payload.replace(/\n?$/, '\n')}\n`;
|
||||||
const escape = (s) => s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
||||||
|
|
||||||
el.innerHTML = String(payload2)
|
// Use text node to preserve code fences better
|
||||||
.split('\n')
|
const textNode = document.createTextNode(payload2);
|
||||||
.map(line => line.length ? `<p>${escape(line)}</p>` : '<p><br></p>')
|
el.innerHTML = '';
|
||||||
.join('');
|
el.appendChild(textNode);
|
||||||
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 }));
|
||||||
RC_DEBUG?.info('✅ Paste method succeeded: ProseMirror');
|
RC_DEBUG?.info('✅ Paste method succeeded: ProseMirror');
|
||||||
|
|
@ -1478,34 +1597,90 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pasteAndMaybeSubmit(text) {
|
async function pasteAndMaybeSubmit(text, attempt = 0, startedAt = Date.now(), submitRetry = 0) {
|
||||||
const ready = await waitForComposerReady({ timeoutMs: CONFIG.QUEUE_WAIT_FOR_COMPOSER_MS });
|
// 1) Check if elapsed time exceeds MAX_COMPOSER_WAIT_MS
|
||||||
if (!ready) {
|
const elapsed = Date.now() - startedAt;
|
||||||
RC_DEBUG?.warn('Composer not ready; re-queueing paste');
|
if (elapsed > CONFIG.MAX_COMPOSER_WAIT_MS) {
|
||||||
execQueue.push(async () => { await pasteAndMaybeSubmit(text); });
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pasted = pasteToComposer(text);
|
// 2) Quick readiness probe with 1200ms timeout
|
||||||
if (!pasted) return false;
|
const ready = await waitForComposerReady({ timeoutMs: 1200 });
|
||||||
|
if (!ready) {
|
||||||
try {
|
// 3) Not ready, requeue with exponential backoff (600ms base, cap at 30s)
|
||||||
const el = getVisibleInputCandidate();
|
const backoffMs = Math.min(30_000, Math.floor(600 * Math.pow(1.6, attempt)));
|
||||||
const actualContent = el?.textContent || el?.value || '[no content found]';
|
RC_DEBUG?.warn('Composer not ready; re-queueing paste with backoff', {
|
||||||
RC_DEBUG?.info('📋 Content in composer after paste', {
|
attempt,
|
||||||
expectedLength: text.length,
|
backoffMs,
|
||||||
actualLength: actualContent.length,
|
elapsed
|
||||||
actualPreview: actualContent.substring(0, 200)
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
setTimeout(() => {
|
||||||
RC_DEBUG?.warn('Could not read composer content', { error: String(e) });
|
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;
|
if (!CONFIG.AUTO_SUBMIT) return true;
|
||||||
|
|
||||||
|
// 5) After paste, wait POST_PASTE_DELAY_MS
|
||||||
await ExecutionManager.delay(CONFIG.POST_PASTE_DELAY_MS);
|
await ExecutionManager.delay(CONFIG.POST_PASTE_DELAY_MS);
|
||||||
|
|
||||||
|
// 6) Try submitComposer()
|
||||||
const ok = await submitComposer();
|
const ok = await submitComposer();
|
||||||
if (!ok) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1743,7 +1918,7 @@
|
||||||
if (command.action === 'get_file') {
|
if (command.action === 'get_file') {
|
||||||
const body = this._extractGetFileBody(data);
|
const body = this._extractGetFileBody(data);
|
||||||
if (typeof body === 'string' && body.length) {
|
if (typeof body === 'string' && body.length) {
|
||||||
new ResponseBuffer().push({ label, content: body });
|
(window.AI_REPO_RESPONSES || new ResponseBuffer()).push({ label, content: body });
|
||||||
} else {
|
} else {
|
||||||
GM_notification({ title: 'AI Repo Commander', text: 'get_file succeeded, but no content to paste.', timeout: 4000 });
|
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);
|
const files = this._extractFilesArray(data);
|
||||||
if (files && files.length) {
|
if (files && files.length) {
|
||||||
const listing = this._formatFilesListing(files);
|
const listing = this._formatFilesListing(files);
|
||||||
new ResponseBuffer().push({ label, content: listing });
|
(window.AI_REPO_RESPONSES || new ResponseBuffer()).push({ label, content: listing });
|
||||||
} else {
|
} else {
|
||||||
const fallback = '```json\n' + JSON.stringify(data, null, 2) + '\n```';
|
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({
|
GM_notification({
|
||||||
title: 'AI Repo Commander',
|
title: 'AI Repo Commander',
|
||||||
text: 'list_files succeeded, but response had no obvious files array. Pasted raw JSON.',
|
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 };
|
return { success: true, data, isMock };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2070,6 +2254,7 @@
|
||||||
// MutationObserver for immediate detection - watching edits AND additions
|
// MutationObserver for immediate detection - watching edits AND additions
|
||||||
this.observer = new MutationObserver((mutations) => {
|
this.observer = new MutationObserver((mutations) => {
|
||||||
let shouldScan = false;
|
let shouldScan = false;
|
||||||
|
let adjacentToProcessed = false;
|
||||||
let reasons = new Set();
|
let reasons = new Set();
|
||||||
|
|
||||||
for (const m of mutations) {
|
for (const m of mutations) {
|
||||||
|
|
@ -2115,7 +2300,33 @@
|
||||||
if (shouldScan) break;
|
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(', ') });
|
RC_DEBUG?.trace('MO: scan triggered', { reasons: Array.from(reasons).join(', ') });
|
||||||
scheduleScan();
|
scheduleScan();
|
||||||
}
|
}
|
||||||
|
|
@ -2256,7 +2467,27 @@
|
||||||
const subId = `${messageId}#${idx + 1}`;
|
const subId = `${messageId}#${idx + 1}`;
|
||||||
|
|
||||||
execQueue.push(async () => {
|
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;
|
let parsed;
|
||||||
try {
|
try {
|
||||||
parsed = CommandParser.parseYAMLCommand(finalTxt);
|
parsed = CommandParser.parseYAMLCommand(finalTxt);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue