fixed all warnings and the docs

This commit is contained in:
rob 2025-10-15 15:56:21 -03:00
parent 7a441aee48
commit 6d6d8a094a
1 changed files with 2857 additions and 2716 deletions

View File

@ -105,22 +105,23 @@
const raw = localStorage.getItem(STORAGE_KEYS.cfg);
if (!raw) return structuredClone(DEFAULT_CONFIG);
const saved = JSON.parse(raw);
// Always use current script's VERSION and RUNTIME, not stale values from storage
delete saved.VERSION;
delete saved.RUNTIME;
return { ...DEFAULT_CONFIG, ...saved, RUNTIME: { ...DEFAULT_CONFIG.RUNTIME, ...(saved.RUNTIME || {}) } };
} catch {
return structuredClone(DEFAULT_CONFIG);
}
}
function saveConfig(cfg) {
try { localStorage.setItem(STORAGE_KEYS.cfg, JSON.stringify(cfg)); } catch {}
try {
const { VERSION, RUNTIME, ...persistable } = cfg;
localStorage.setItem(STORAGE_KEYS.cfg, JSON.stringify(persistable));
} catch {}
}
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;
@ -516,6 +517,7 @@
const dump = JSON.parse(JSON.stringify(this.cfg));
if (dump.BRIDGE_KEY) dump.BRIDGE_KEY = '•'.repeat(8);
dump.VERSION = DEFAULT_CONFIG.VERSION;
root.querySelector('.rc-json').value = JSON.stringify(dump, null, 2);
const bridgeKeyInput = root.querySelector('.rc-bridge-key');
@ -615,6 +617,10 @@
delete parsed.BRIDGE_KEY;
}
// Prevent overriding ephemeral fields from pasted JSON
delete parsed.VERSION;
delete parsed.RUNTIME;
Object.assign(this.cfg, parsed);
saveConfig(this.cfg);
@ -825,13 +831,32 @@
}
function findAllCommandsInMessage(el) {
const blocks = el.querySelectorAll('pre code, pre, code');
const hits = [];
const seen = new Set();
// 1) First scan code elements (pre code, pre, code)
const blocks = el.querySelectorAll('pre code, pre, code');
for (const b of blocks) {
const txt = (b.textContent || '').trim();
const parts = extractAllCompleteBlocks(txt);
for (const part of parts) hits.push({ blockElement: b, text: `@bridge@\n${part}\n@end@` });
for (const part of parts) {
const normalized = _norm(part);
seen.add(normalized);
hits.push({ blockElement: b, text: `@bridge@\n${part}\n@end@` });
}
}
// 2) Also scan the entire element's textContent for plain-text blocks
const wholeText = _norm(el.textContent || '');
const plainParts = extractAllCompleteBlocks(wholeText);
for (const part of plainParts) {
const normalized = _norm(part);
if (!seen.has(normalized)) {
seen.add(normalized);
hits.push({ blockElement: null, text: `@bridge@\n${part}\n@end@` });
}
}
return hits;
}
@ -943,29 +968,39 @@
continue;
}
const el = getVisibleInputCandidate();
// Pre-paste: the send button can be disabled. Don't block on it here.
const btn = findSendButton(el);
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;
// 1) Add typing indicator to busy selector
// Narrow scope to composer's local container
const scope = el?.closest('form, [data-testid="composer"], [data-testid="composer-container"], main, body') || document;
// 1) Narrow "busy" detection to the scope and ignore hidden spinners
const busy = scope.querySelector('[aria-busy="true"], [data-state="loading"], .typing-indicator');
// 2) Check if composer has unsent content
// Debug logging when composer or button not found
if (!el || !btn) {
RC_DEBUG?.verbose('Composer probe', {
foundEl: !!el,
elTag: el?.tagName,
elClasses: el ? Array.from(el.classList || []).join(' ') : null,
hasBtn: !!btn,
busyFound: !!busy
});
}
// 2) Only block if there is real (non-whitespace) unsent content already present
let hasUnsent = false;
if (el) {
try {
const currentText = (el.textContent || el.value || '').trim();
if (currentText.startsWith('@bridge@') || currentText.startsWith('### [')) {
hasUnsent = true;
}
hasUnsent = currentText.length > 0 && !/^\s*$/.test(currentText);
} catch (e) {
RC_DEBUG?.verbose('Failed to check composer content', { error: String(e) });
}
}
if (el && btnReady && !busy && !hasUnsent) return true;
// Ready to paste as soon as composer exists, not busy, and no unsent text.
if (el && !busy && !hasUnsent) return true;
await ExecutionManager.delay(pollMs);
}
RC_DEBUG?.warn('Composer not ready within timeout');
@ -1387,12 +1422,24 @@
// ---------------------- Paste + Submit helpers ----------------------
function getVisibleInputCandidate() {
const candidates = [
'.ProseMirror#prompt-textarea',
'#prompt-textarea.ProseMirror',
// ChatGPT / GPT
'#prompt-textarea',
'.ProseMirror',
'[contenteditable="true"]',
'textarea'
'.ProseMirror#prompt-textarea',
'.ProseMirror[role="textbox"][contenteditable="true"]',
'[data-testid="composer"] [contenteditable="true"][role="textbox"]',
'main [contenteditable="true"][role="textbox"]',
// Claude
'.chat-message + [contenteditable="true"]',
'[contenteditable="true"][data-testid="chat-input"]',
// Gemini
'textarea[data-testid="input-area"]',
'[contenteditable="true"][aria-label*="Message"]',
// Generic fallbacks
'textarea',
'[contenteditable="true"]'
];
for (const sel of candidates) {
const el = document.querySelector(sel);
@ -1406,24 +1453,98 @@
}
function findSendButton(scopeEl) {
const scope = scopeEl?.closest('form, [data-testid="composer"], main') || document;
// Try multiple scope strategies
const formScope = scopeEl?.closest('form');
const composerScope = scopeEl?.closest('[data-testid="composer"]');
const mainScope = scopeEl?.closest('main');
const scope = formScope || composerScope || mainScope || document;
// Debug: Log scoping information
RC_DEBUG?.verbose('findSendButton: scope resolution', {
hasScopeEl: !!scopeEl,
scopeElTag: scopeEl?.tagName,
formScope: !!formScope,
composerScope: !!composerScope,
mainScope: !!mainScope,
usingDocument: scope === document
});
const selectors = [
'button[data-testid="send-button"]',
'button#composer-submit-button',
'[id="composer-submit-button"]',
'button.composer-submit-btn',
'button[data-testid="composer-send-button"]',
'button[aria-label="Send"]',
'button[aria-label*="Send prompt"]',
'button[aria-label*="Send message"]',
'button[aria-label*="Send"]',
'button[aria-label*="send"]',
'button[aria-label*="Submit"]',
'button[aria-label*="submit"]',
'form button[type="submit"]'
// Some pages omit type=submit; keep generic button-in-form last
'form button'
];
// First pass: Try scoped search
for (const s of selectors) {
const btn = scope.querySelector(s);
if (!btn) continue;
if (btn) {
const style = window.getComputedStyle(btn);
const disabled = btn.disabled || btn.getAttribute('aria-disabled') === 'true';
if (style.display === 'none' || style.visibility === 'hidden') continue;
if (btn.offsetParent === null && style.position !== 'fixed') continue;
if (!disabled) return btn;
const hidden = style.display === 'none' || style.visibility === 'hidden';
const notRendered = btn.offsetParent === null && style.position !== 'fixed';
RC_DEBUG?.verbose('findSendButton: found candidate (scoped)', {
selector: s,
id: btn.id,
disabled,
hidden,
notRendered,
willReturn: !disabled && !hidden && !notRendered
});
if (!disabled && !hidden && !notRendered) return btn;
}
}
// Second pass: Fallback to global search with detailed logging
RC_DEBUG?.verbose('findSendButton: no button found in scope, trying global search');
for (const s of selectors) {
const btn = document.querySelector(s);
if (btn) {
const style = window.getComputedStyle(btn);
const disabled = btn.disabled || btn.getAttribute('aria-disabled') === 'true';
const hidden = style.display === 'none' || style.visibility === 'hidden';
const notRendered = btn.offsetParent === null && style.position !== 'fixed';
RC_DEBUG?.verbose('findSendButton: found candidate (global)', {
selector: s,
id: btn.id,
disabled,
hidden,
notRendered,
inScope: scope.contains(btn),
willReturn: !disabled && !hidden && !notRendered
});
if (!disabled && !hidden && !notRendered) return btn;
}
}
// Final fallback: XPath for exact button location (works if structure hasn't drifted)
try {
const xp = '/html/body/div[1]/div/div/div[2]/main/div/div/div[2]/div[1]/div/div[2]/form/div[2]/div/div[3]/div/button';
const node = document.evaluate(xp, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if (node instanceof HTMLButtonElement) {
RC_DEBUG?.verbose('findSendButton: found via XPath fallback', { id: node.id });
return node;
}
} catch (e) {
RC_DEBUG?.verbose('findSendButton: XPath fallback failed', { error: String(e) });
}
RC_DEBUG?.warn('findSendButton: no valid button found anywhere');
return null;
}
@ -1790,6 +1911,114 @@
}
// ---------------------- ResponseBuffer (helper functions and class) ----------------------
function chunkByLines(s, limit) {
const out = [];
let start = 0;
while (start < s.length) {
const endSoft = s.lastIndexOf('\n', Math.min(start + limit, s.length));
const end = endSoft > start ? endSoft + 1 : Math.min(start + limit, s.length);
out.push(s.slice(start, end));
start = end;
}
return out;
}
function isSingleFencedBlock(s) {
return /^```[^\n]*\n[\s\S]*\n```$/.test(s.trim());
}
function splitRespectingCodeFence(text, limit) {
const trimmed = text.trim();
if (!isSingleFencedBlock(trimmed)) {
// Not a single fence → just line-friendly chunking
return chunkByLines(text, limit);
}
// Extract inner payload & language hint
const m = /^```([^\n]*)\n([\s\S]*)\n```$/.exec(trimmed);
const lang = (m?.[1] || 'text').trim();
const inner = m?.[2] ?? '';
const chunks = chunkByLines(inner, limit - 16 - lang.length); // budget for fences
return chunks.map(c => '```' + lang + '\n' + c.replace(/\n?$/, '\n') + '```');
}
class ResponseBuffer {
constructor() {
this.pending = []; // { label, content }
this.timer = null;
this.flushing = false;
}
push(item) {
if (!item || !item.content) return;
this.pending.push(item);
this.scheduleFlush();
}
scheduleFlush() {
if (this.timer) clearTimeout(this.timer);
this.timer = setTimeout(() => this.flush(), CONFIG.RESPONSE_BUFFER_FLUSH_DELAY_MS || 500);
}
buildCombined() {
const parts = [];
for (const { label, content } of this.pending) {
if (CONFIG.RESPONSE_BUFFER_SECTION_HEADINGS && label) {
parts.push(`### ${label}\n`);
}
parts.push(String(content).trimEnd());
parts.push(''); // blank line between sections
}
return parts.join('\n');
}
async flush() {
if (this.flushing) return;
if (!this.pending.length) return;
this.flushing = true;
const toPaste = this.buildCombined();
this.pending.length = 0; // clear
try {
const limit = CONFIG.MAX_PASTE_CHARS || 250_000;
if (CONFIG.SPLIT_LONG_RESPONSES && toPaste.length > limit) {
const chunks = splitRespectingCodeFence(toPaste, limit);
RC_DEBUG?.warn(`Splitting long response into ${chunks.length} message(s)`, {
totalChars: toPaste.length, perChunkLimit: limit
});
chunks.forEach((chunk, i) => {
const header = CONFIG.RESPONSE_BUFFER_SECTION_HEADINGS
? `### Part ${i+1}/${chunks.length}\n`
: '';
const payload = header + chunk;
execQueue.push(async () => {
await pasteAndMaybeSubmit(payload);
});
});
return; // done: queued as multiple messages
}
// Normal single-message path
execQueue.push(async () => {
await pasteAndMaybeSubmit(toPaste);
});
} finally {
this.flushing = false;
}
}
}
// Initialize singleton
window.AI_REPO_RESPONSES = new ResponseBuffer();
// ---------------------- Execution ----------------------
class ExecutionManager {
static async executeCommand(command, sourceElement, renderKey = '', label = '') {
@ -1918,7 +2147,7 @@
if (command.action === 'get_file') {
const body = this._extractGetFileBody(data);
if (typeof body === 'string' && body.length) {
(window.AI_REPO_RESPONSES || new ResponseBuffer()).push({ label, content: body });
window.AI_REPO_RESPONSES.push({ label, content: body });
} else {
GM_notification({ title: 'AI Repo Commander', text: 'get_file succeeded, but no content to paste.', timeout: 4000 });
}
@ -1928,10 +2157,10 @@
const files = this._extractFilesArray(data);
if (files && files.length) {
const listing = this._formatFilesListing(files);
(window.AI_REPO_RESPONSES || new ResponseBuffer()).push({ label, content: listing });
window.AI_REPO_RESPONSES.push({ label, content: listing });
} else {
const fallback = '```json\n' + JSON.stringify(data, null, 2) + '\n```';
(window.AI_REPO_RESPONSES || new ResponseBuffer()).push({ label, content: fallback });
window.AI_REPO_RESPONSES.push({ label, content: fallback });
GM_notification({
title: 'AI Repo Commander',
text: 'list_files succeeded, but response had no obvious files array. Pasted raw JSON.',
@ -2020,114 +2249,6 @@
cancelOne: (cb) => execQueue.cancelOne(cb),
};
function chunkByLines(s, limit) {
const out = [];
let start = 0;
while (start < s.length) {
const endSoft = s.lastIndexOf('\n', Math.min(start + limit, s.length));
const end = endSoft > start ? endSoft + 1 : Math.min(start + limit, s.length);
out.push(s.slice(start, end));
start = end;
}
return out;
}
function isSingleFencedBlock(s) {
return /^```[^\n]*\n[\s\S]*\n```$/.test(s.trim());
}
function splitRespectingCodeFence(text, limit) {
const trimmed = text.trim();
if (!isSingleFencedBlock(trimmed)) {
// Not a single fence → just line-friendly chunking
return chunkByLines(text, limit);
}
// Extract inner payload & language hint
const m = /^```([^\n]*)\n([\s\S]*)\n```$/.exec(trimmed);
const lang = (m?.[1] || 'text').trim();
const inner = m?.[2] ?? '';
const chunks = chunkByLines(inner, limit - 16 - lang.length); // budget for fences
return chunks.map(c => '```' + lang + '\n' + c.replace(/\n?$/, '\n') + '```');
}
// ---------------------- ResponseBuffer ----------------------
class ResponseBuffer {
constructor() {
this.pending = []; // { label, content }
this.timer = null;
this.flushing = false;
}
push(item) {
if (!item || !item.content) return;
this.pending.push(item);
this.scheduleFlush();
}
scheduleFlush() {
if (this.timer) clearTimeout(this.timer);
this.timer = setTimeout(() => this.flush(), CONFIG.RESPONSE_BUFFER_FLUSH_DELAY_MS || 500);
}
buildCombined() {
const parts = [];
for (const { label, content } of this.pending) {
if (CONFIG.RESPONSE_BUFFER_SECTION_HEADINGS && label) {
parts.push(`### ${label}\n`);
}
parts.push(String(content).trimEnd());
parts.push(''); // blank line between sections
}
return parts.join('\n');
}
async flush() {
if (this.flushing) return;
if (!this.pending.length) return;
this.flushing = true;
const toPaste = this.buildCombined();
this.pending.length = 0; // clear
try {
const limit = CONFIG.MAX_PASTE_CHARS || 250_000;
if (CONFIG.SPLIT_LONG_RESPONSES && toPaste.length > limit) {
const chunks = splitRespectingCodeFence(toPaste, limit);
RC_DEBUG?.warn(`Splitting long response into ${chunks.length} message(s)`, {
totalChars: toPaste.length, perChunkLimit: limit
});
chunks.forEach((chunk, i) => {
const header = CONFIG.RESPONSE_BUFFER_SECTION_HEADINGS
? `### Part ${i+1}/${chunks.length}\n`
: '';
const payload = header + chunk;
execQueue.push(async () => {
await pasteAndMaybeSubmit(payload);
});
});
return; // done: queued as multiple messages
}
// Normal single-message path
execQueue.push(async () => {
await pasteAndMaybeSubmit(toPaste);
});
} finally {
this.flushing = false;
}
}
}
window.AI_REPO_RESPONSES = new ResponseBuffer(); // optional debug handle
// ---------------------- Bridge Key ----------------------
let BRIDGE_KEY = null;
@ -2176,6 +2297,37 @@
}
};
// Helper function to find command text in an element (code blocks or plain text)
function findCommandTextInElement(el) {
// Helper to check if text is a complete command
const isComplete = (txt) => {
if (!CONFIG.REQUIRE_TERMINATOR) return /(^|\n)\s*@bridge@\b/m.test(txt) && /(^|\n)\s*action\s*:/m.test(txt);
return /(^|\n)\s*@bridge@\b/m.test(txt)
&& /(^|\n)\s*action\s*:/m.test(txt)
&& /@end@\s*$/m.test(txt);
};
// 1) First try to find in code blocks
const blocks = el.querySelectorAll('pre code, pre, code');
for (const b of blocks) {
const txt = (b.textContent || '').trim();
if (isComplete(txt)) {
return { blockElement: b, text: txt };
}
}
// 2) If not found in code blocks, check raw message text
const wholeText = _norm(el.textContent || '');
const parts = extractAllCompleteBlocks(wholeText);
if (parts.length > 0) {
const part = parts[0];
return { blockElement: null, text: `@bridge@\n${part}\n@end@` };
}
// 3) No complete command found
return null;
}
// ---------------------- Monitor (with streaming "settle" & complete-block check) ----------------------
class CommandMonitor {
constructor() {
@ -2375,18 +2527,7 @@
}
findCommandInCodeBlock(el) {
const blocks = el.querySelectorAll('pre code, pre, code');
// 🔍 LOG: What we found
RC_DEBUG?.trace('🔍 DOM: Searching for command block', {
blocksFound: blocks.length
});
for (const b of blocks) {
const txt = (b.textContent || '').trim();
if (this.isCompleteCommandText(txt)) {
return { blockElement: b, text: txt };
}
}
return null;
return findCommandTextInElement(el);
}
scanMessages() {
@ -2937,7 +3078,7 @@
// ---------------------- Test commands ----------------------
const TEST_COMMANDS = {
validUpdate:
`\
`\
\`\`\`yaml
@bridge@
action: update_file
@ -2952,7 +3093,7 @@ content: |
\`\`\`
`,
getFile:
`\
`\
\`\`\`yaml
@bridge@
action: get_file
@ -2962,7 +3103,7 @@ path: README.md
\`\`\`
`,
listFiles:
`\
`\
\`\`\`yaml
@bridge@
action: list_files
@ -2972,7 +3113,7 @@ path: .
\`\`\`
`,
createBranch:
`\
`\
\`\`\`yaml
@bridge@
action: create_branch
@ -2983,7 +3124,7 @@ source_branch: main
\`\`\`
`,
createPR:
`\
`\
\`\`\`yaml
@bridge@
action: create_pr
@ -2999,7 +3140,7 @@ body: |
\`\`\`
`,
createIssue:
`\
`\
\`\`\`yaml
@bridge@
action: create_issue
@ -3014,7 +3155,7 @@ body: |
\`\`\`
`,
createTag:
`\
`\
\`\`\`yaml
@bridge@
action: create_tag
@ -3026,7 +3167,7 @@ message: Release version 1.0.0
\`\`\`
`,
createRelease:
`\
`\
\`\`\`yaml
@bridge@
action: create_release
@ -3045,7 +3186,7 @@ body: |
\`\`\`
`,
multiCommand:
`\
`\
\`\`\`yaml
@bridge@
action: get_file