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)); 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) { function _ordinalForKey(el, key) {
const list = Array.from(document.querySelectorAll(MSG_SELECTORS.join(','))); const list = Array.from(document.querySelectorAll(MSG_SELECTORS.join(',')));
let n = 0; let n = 0;
@ -776,7 +786,8 @@
// Compute on the fly only if needed // Compute on the fly only if needed
const ch = _hashCommand(node); const ch = _hashCommand(node);
const ph = _hashPrevContext(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 (nodeKey === key) n++;
if (node === el) return n; // 1-based ordinal if (node === el) return n; // 1-based ordinal
@ -802,8 +813,9 @@
// Always use content-based fingerprinting for reliability across reloads // Always use content-based fingerprinting for reliability across reloads
const ch = _hashCommand(el); const ch = _hashCommand(el);
const ph = _hashPrevContext(el); const ph = _hashPrevContext(el);
const ih = _hashIntraMessagePrefix(el);
const dh = _hash(_domHint(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 n = _ordinalForKey(el, key);
const fingerprint = `${key}|hint:${dh}|n:${n}`; const fingerprint = `${key}|hint:${dh}|n:${n}`;
@ -811,6 +823,7 @@
fingerprint: fingerprint.slice(0, 60) + '...', fingerprint: fingerprint.slice(0, 60) + '...',
commandHash: ch, commandHash: ch,
prevContextHash: ph, prevContextHash: ph,
intraMessageHash: ih,
domHint: dh, domHint: dh,
ordinal: n ordinal: n
}); });
@ -818,6 +831,15 @@
return fingerprint; 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 ---------------------- // ---------------------- Multi-block extraction helpers ----------------------
function extractAllCompleteBlocks(text) { function extractAllCompleteBlocks(text) {
const out = []; const out = [];
@ -944,18 +966,24 @@
RC_DEBUG?.verbose('Cluster rescan completed', { scanned, deadline: Date.now() >= deadline }); 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 // 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; let badge = el.querySelector('.ai-rc-queue-badge');
const badge = document.createElement('span'); if (!badge) {
badge = document.createElement('span');
badge.className = 'ai-rc-queue-badge'; badge.className = 'ai-rc-queue-badge';
badge.textContent = `${count} command${count>1?'s':''} queued`;
badge.style.cssText = ` badge.style.cssText = `
display:inline-block; padding:2px 6px; margin:4px 0; display:inline-block; padding:2px 6px; margin:4px 0;
background:#3b82f6; color:#fff; border-radius:4px; background:#3b82f6; color:#fff; border-radius:4px;
font:11px ui-monospace, monospace;`; font:11px ui-monospace, monospace;`;
el.insertBefore(badge, el.firstChild); el.insertBefore(badge, el.firstChild);
} }
badge.textContent = `${count} command${count>1?'s':''} queued`;
}
// Wait until it's safe to paste/submit // Wait until it's safe to paste/submit
async function waitForComposerReady({ timeoutMs = CONFIG.QUEUE_WAIT_FOR_COMPOSER_MS, pollMs = 200 } = {}) { async function waitForComposerReady({ timeoutMs = CONFIG.QUEUE_WAIT_FOR_COMPOSER_MS, pollMs = 200 } = {}) {
@ -1173,7 +1201,7 @@
* @param {string|number} [suffix] * @param {string|number} [suffix]
*/ */
hasElement(el, suffix = '') { hasElement(el, suffix = '') {
let fp = fingerprintElement(el); let fp = getStableFingerprint(el);
if (suffix !== '' && suffix != null) fp += `#${String(suffix)}`; if (suffix !== '' && suffix != null) fp += `#${String(suffix)}`;
const result = this.session.has(fp) || (fp in this.cache); const result = this.session.has(fp) || (fp in this.cache);
@ -1193,12 +1221,15 @@
* @param {string|number} [suffix] * @param {string|number} [suffix]
*/ */
markElement(el, suffix = '') { markElement(el, suffix = '') {
let fp = fingerprintElement(el); let fp = getStableFingerprint(el);
if (suffix !== '' && suffix != null) fp += `#${String(suffix)}`; if (suffix !== '' && suffix != null) fp += `#${String(suffix)}`;
this.session.add(fp); this.session.add(fp);
this.cache[fp] = Date.now(); this.cache[fp] = Date.now();
this._save(); 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', { RC_DEBUG?.verbose('Marked element as processed', {
fingerprint: fp.slice(0, 60) + '...' fingerprint: fp.slice(0, 60) + '...'
}); });
@ -2172,7 +2203,12 @@
// Trigger cluster rescan for chainable commands // Trigger cluster rescan for chainable commands
try { try {
if (shouldTriggerClusterRescan(sourceElement, command.action)) { 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); await scheduleClusterRescan(sourceElement);
} else {
RC_DEBUG?.verbose('Cluster rescan suppressed by cooldown');
}
} }
} catch (e) { } catch (e) {
RC_DEBUG?.verbose('Cluster rescan failed', { error: String(e) }); RC_DEBUG?.verbose('Cluster rescan failed', { error: String(e) });
@ -2541,62 +2577,99 @@
messages.forEach((el) => { messages.forEach((el) => {
if (!this.isAssistantMessage(el)) return; 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); const hits = findAllCommandsInMessage(el);
if (!hits.length) return; 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'; 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; return;
} }
this.history.markElement(el, 1); 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; return;
} }
// Check if within cold start or all already executed
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 = (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) { if (withinColdStart || alreadyAll) {
el.dataset.aiRcProcessed = '1';
RC_DEBUG?.verbose( RC_DEBUG?.verbose(
'Skipping command(s) - ' + 'Skipping command(s) - ' +
(withinColdStart ? 'page load (cold start)' : 'already executed in this conversation'), (withinColdStart ? 'page load (cold start)' : 'already executed in this conversation'),
{ fingerprint: fingerprintElement(el).slice(0, 40) + '...', commandCount: hits.length } { 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'; el.dataset.aiRcProcessed = '1';
const hit2 = findAllCommandsInMessage(el).slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE); const hit2 = findAllCommandsInMessage(el).slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE);
const h = hit2[idx]; const h = hit2[idx];
if (h) this.enqueueCommand(el, h, idx); if (h) this.enqueueCommand(el, h, idx);
}); });
skipped += hits.length; skipped += capped.length;
return; return;
} }
// New message that hasn't been executed → auto-execute once // Multi-block: mark & enqueue all we see now
el.dataset.aiRcProcessed = '1';
const capped = hits.slice(0, CONFIG.QUEUE_MAX_PER_MESSAGE);
attachQueueBadge(el, capped.length); attachQueueBadge(el, capped.length);
capped.forEach((hit, idx) => { capped.forEach((hit, idx) => {
// mark each sub-command immediately to avoid re-exec on reloads const subIdx = idx + 1;
this.history.markElement(el, 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); this.enqueueCommand(el, hit, idx);
}); });
found += capped.length; 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`); if (skipped) RC_DEBUG?.info(`Skipped ${skipped} command(s) - Run Again buttons added`);
@ -2605,7 +2678,16 @@
enqueueCommand(element, hit, idx) { enqueueCommand(element, hit, idx) {
const messageId = this.getReadableMessageId(element); 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 () => { execQueue.push(async () => {
// Micro-settle: wait for text to stabilize before parsing // Micro-settle: wait for text to stabilize before parsing
@ -2613,8 +2695,8 @@
const blockElement = hit.blockElement; const blockElement = hit.blockElement;
if (blockElement) { if (blockElement) {
let lastText = blockElement.textContent || ''; let lastText = blockElement.textContent || '';
const maxWait = 400; const maxWait = 700;
const checkInterval = 80; const checkInterval = 70;
const startTime = Date.now(); const startTime = Date.now();
while (Date.now() - startTime < maxWait) { while (Date.now() - startTime < maxWait) {
@ -2973,6 +3055,13 @@
this.updateState(messageId, COMMAND_STATES.COMPLETE); 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) { } catch (error) {
const duration = Date.now() - started; const duration = Date.now() - started;
RC_DEBUG?.error(`Command processing error: ${error.message}`, { messageId, duration }); RC_DEBUG?.error(`Command processing error: ${error.message}`, { messageId, duration });