fixed all warnings and the docs

This commit is contained in:
rob 2025-10-15 19:09:01 -03:00
parent 6d6d8a094a
commit ca94e03289
1 changed files with 144 additions and 55 deletions

View File

@ -765,7 +765,17 @@
return _hash(buf.slice(-2000));
}
// Ordinal among messages that share the same (commandHash, prevCtxHash)
// Hash the text within this message that appears BEFORE the first command block
function _hashIntraMessagePrefix(el) {
const t = (el.textContent || '');
// Find the first complete @bridge@ block
const m = t.match(/@bridge@[\s\S]*?@end@/m);
const endIdx = m ? t.indexOf(m[0]) : t.length;
// Hash the last 2000 chars before the command block
return _hash(_norm(t.slice(Math.max(0, endIdx - 2000), endIdx)));
}
// Ordinal among messages that share the same (commandHash, prevCtxHash, intraHash)
function _ordinalForKey(el, key) {
const list = Array.from(document.querySelectorAll(MSG_SELECTORS.join(',')));
let n = 0;
@ -776,7 +786,8 @@
// Compute on the fly only if needed
const ch = _hashCommand(node);
const ph = _hashPrevContext(node);
return `ch:${ch}|ph:${ph}`;
const ih = _hashIntraMessagePrefix(node);
return `ch:${ch}|ph:${ph}|ih:${ih}`;
})();
if (nodeKey === key) n++;
if (node === el) return n; // 1-based ordinal
@ -802,8 +813,9 @@
// Always use content-based fingerprinting for reliability across reloads
const ch = _hashCommand(el);
const ph = _hashPrevContext(el);
const ih = _hashIntraMessagePrefix(el);
const dh = _hash(_domHint(el));
const key = `ch:${ch}|ph:${ph}`;
const key = `ch:${ch}|ph:${ph}|ih:${ih}`;
const n = _ordinalForKey(el, key);
const fingerprint = `${key}|hint:${dh}|n:${n}`;
@ -811,6 +823,7 @@
fingerprint: fingerprint.slice(0, 60) + '...',
commandHash: ch,
prevContextHash: ph,
intraMessageHash: ih,
domHint: dh,
ordinal: n
});
@ -818,6 +831,15 @@
return fingerprint;
}
// Stable fingerprint: computed once per element, then cached on dataset.
// Prevents drift when the DOM/text changes later.
function getStableFingerprint(el) {
if (el?.dataset?.aiRcStableFp) return el.dataset.aiRcStableFp;
const fp = fingerprintElement(el);
try { if (el && el.dataset) el.dataset.aiRcStableFp = fp; } catch {}
return fp;
}
// ---------------------- Multi-block extraction helpers ----------------------
function extractAllCompleteBlocks(text) {
const out = [];
@ -944,18 +966,24 @@
RC_DEBUG?.verbose('Cluster rescan completed', { scanned, deadline: Date.now() >= deadline });
}
// Helper functions for per-subcommand dataset flags
function subDoneKey(i) { return `aiRcSubDone_${i}`; } // i is 1-based
function subEnqKey(i) { return `aiRcSubEnq_${i}`; } // i is 1-based
// Tiny badge on the message showing how many got queued
function attachQueueBadge(el, count) {
if (el.querySelector('.ai-rc-queue-badge')) return;
const badge = document.createElement('span');
let badge = el.querySelector('.ai-rc-queue-badge');
if (!badge) {
badge = document.createElement('span');
badge.className = 'ai-rc-queue-badge';
badge.textContent = `${count} command${count>1?'s':''} queued`;
badge.style.cssText = `
display:inline-block; padding:2px 6px; margin:4px 0;
background:#3b82f6; color:#fff; border-radius:4px;
font:11px ui-monospace, monospace;`;
el.insertBefore(badge, el.firstChild);
}
badge.textContent = `${count} command${count>1?'s':''} queued`;
}
// Wait until it's safe to paste/submit
async function waitForComposerReady({ timeoutMs = CONFIG.QUEUE_WAIT_FOR_COMPOSER_MS, pollMs = 200 } = {}) {
@ -1173,7 +1201,7 @@
* @param {string|number} [suffix]
*/
hasElement(el, suffix = '') {
let fp = fingerprintElement(el);
let fp = getStableFingerprint(el);
if (suffix !== '' && suffix != null) fp += `#${String(suffix)}`;
const result = this.session.has(fp) || (fp in this.cache);
@ -1193,12 +1221,15 @@
* @param {string|number} [suffix]
*/
markElement(el, suffix = '') {
let fp = fingerprintElement(el);
let fp = getStableFingerprint(el);
if (suffix !== '' && suffix != null) fp += `#${String(suffix)}`;
this.session.add(fp);
this.cache[fp] = Date.now();
this._save();
// Also set hard per-subcommand flag on element (bullet-proof local dedupe)
try { if (el && el.dataset && suffix) el.dataset[subDoneKey(Number(suffix))] = '1'; } catch {}
RC_DEBUG?.verbose('Marked element as processed', {
fingerprint: fp.slice(0, 60) + '...'
});
@ -2172,7 +2203,12 @@
// Trigger cluster rescan for chainable commands
try {
if (shouldTriggerClusterRescan(sourceElement, command.action)) {
if (!sourceElement.dataset.aiRcClusterCoolUntil || Date.now() > Number(sourceElement.dataset.aiRcClusterCoolUntil)) {
sourceElement.dataset.aiRcClusterCoolUntil = String(Date.now() + 1500);
await scheduleClusterRescan(sourceElement);
} else {
RC_DEBUG?.verbose('Cluster rescan suppressed by cooldown');
}
}
} catch (e) {
RC_DEBUG?.verbose('Cluster rescan failed', { error: String(e) });
@ -2541,62 +2577,99 @@
messages.forEach((el) => {
if (!this.isAssistantMessage(el)) return;
if (el.dataset.aiRcProcessed) return;
// Allow re-scan of already-processed messages to catch *new* blocks appended later
const hits = findAllCommandsInMessage(el);
if (!hits.length) return;
if (hits.length === 1) {
const capped = hits.slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE);
// Count how many sub-commands we have already marked for this element
// Prefer element flags (local), fallback to history (persistent)
let already = 0;
for (let i = 0; i < capped.length; i++) {
const idx1 = i + 1;
const done = el?.dataset?.[subDoneKey(idx1)] === '1' || this.history.hasElement(el, idx1);
if (done) already++;
}
// Case A: first time seeing this message (no aiRcProcessed yet)
if (!el.dataset.aiRcProcessed) {
el.dataset.aiRcProcessed = '1';
if (this.history.hasElement(el, 1)) {
attachRunAgainUI(el, () => this.trackMessage(el, hits[0].text, this.getReadableMessageId(el)));
// If only one block, keep fast path
if (capped.length === 1) {
if (already > 0) {
// Already executed, add Run Again button
attachRunAgainUI(el, () => this.trackMessage(el, capped[0].text, this.getReadableMessageId(el)));
skipped++;
return;
}
this.history.markElement(el, 1);
this.trackMessage(el, hits[0].text, this.getReadableMessageId(el));
this.trackMessage(el, capped[0].text, this.getReadableMessageId(el));
found++;
return;
}
// Check if within cold start or all already executed
const withinColdStart = Date.now() < this.coldStartUntil;
const alreadyAll = hits.every((_, i) => this.history.hasElement(el, i + 1));
const alreadyAll = (already === capped.length);
RC_DEBUG?.trace('Evaluating message', {
withinColdStart,
alreadyAll,
commandCount: hits.length
});
// Skip if cold start or already processed (but DON'T mark new ones in history during cold start)
if (withinColdStart || alreadyAll) {
el.dataset.aiRcProcessed = '1';
RC_DEBUG?.verbose(
'Skipping command(s) - ' +
(withinColdStart ? 'page load (cold start)' : 'already executed in this conversation'),
{ fingerprint: fingerprintElement(el).slice(0, 40) + '...', commandCount: hits.length }
);
attachRunAgainPerCommand(el, hits.slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE), (idx) => {
attachRunAgainPerCommand(el, capped, (idx) => {
el.dataset.aiRcProcessed = '1';
const hit2 = findAllCommandsInMessage(el).slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE);
const h = hit2[idx];
if (h) this.enqueueCommand(el, h, idx);
});
skipped += hits.length;
skipped += capped.length;
return;
}
// New message that hasn't been executed → auto-execute once
el.dataset.aiRcProcessed = '1';
const capped = hits.slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE);
// Multi-block: mark & enqueue all we see now
attachQueueBadge(el, capped.length);
capped.forEach((hit, idx) => {
// mark each sub-command immediately to avoid re-exec on reloads
this.history.markElement(el, idx + 1);
const subIdx = idx + 1;
const enqKey = subEnqKey(subIdx);
if (el?.dataset?.[enqKey] === '1' || el?.dataset?.[subDoneKey(subIdx)] === '1') return;
try { if (el && el.dataset) el.dataset[enqKey] = '1'; } catch {}
this.history.markElement(el, subIdx);
this.enqueueCommand(el, hit, idx);
});
found += capped.length;
return;
}
// Case B: message was already processed; enqueue only the *new* ones
if (already < capped.length) {
const newlyAdded = capped.slice(already);
RC_DEBUG?.info('Detected new command blocks in already-processed message', {
alreadyCount: already,
newCount: newlyAdded.length,
totalCount: capped.length
});
const existingQueued = parseInt(el.dataset.aiRcQueued || '0', 10) || 0;
const total = existingQueued + newlyAdded.length;
attachQueueBadge(el, total);
newlyAdded.forEach((hit, idx) => {
const subIdx = already + idx + 1; // 1-based
const enqKey = subEnqKey(subIdx);
if (el?.dataset?.[enqKey] === '1' || el?.dataset?.[subDoneKey(subIdx)] === '1') return;
try { if (el && el.dataset) el.dataset[enqKey] = '1'; } catch {}
this.history.markElement(el, subIdx); // also sets SubDone via patch #2
this.enqueueCommand(el, hit, subIdx - 1);
});
el.dataset.aiRcQueued = String(total);
found += newlyAdded.length;
}
});
if (skipped) RC_DEBUG?.info(`Skipped ${skipped} command(s) - Run Again buttons added`);
@ -2605,7 +2678,16 @@
enqueueCommand(element, hit, idx) {
const messageId = this.getReadableMessageId(element);
const subId = `${messageId}#${idx + 1}`;
const subIndex1 = (idx + 1);
const subId = `${messageId}#${subIndex1}`;
// Hard guard: never enqueue twice
const enqKey = subEnqKey(subIndex1);
if (element?.dataset?.[enqKey] === '1' && element?.dataset?.[subDoneKey(subIndex1)] === '1') {
RC_DEBUG?.verbose('Skip enqueue (already done)', { subIndex1 });
return;
}
try { if (element && element.dataset) element.dataset[enqKey] = '1'; } catch {}
execQueue.push(async () => {
// Micro-settle: wait for text to stabilize before parsing
@ -2613,8 +2695,8 @@
const blockElement = hit.blockElement;
if (blockElement) {
let lastText = blockElement.textContent || '';
const maxWait = 400;
const checkInterval = 80;
const maxWait = 700;
const checkInterval = 70;
const startTime = Date.now();
while (Date.now() - startTime < maxWait) {
@ -2973,6 +3055,13 @@
this.updateState(messageId, COMMAND_STATES.COMPLETE);
// Mark as done on element (belt-and-suspenders against fingerprint drift)
try {
const m = /#(\d+)$/.exec(messageId);
const subIndex1 = m ? Number(m[1]) : 1;
if (message?.element?.dataset) message.element.dataset[subDoneKey(subIndex1)] = '1';
} catch {}
} catch (error) {
const duration = Date.now() - started;
RC_DEBUG?.error(`Command processing error: ${error.message}`, { messageId, duration });