fixed all warnings and the docs
This commit is contained in:
parent
7a441aee48
commit
6d6d8a094a
|
|
@ -105,22 +105,23 @@
|
||||||
const raw = localStorage.getItem(STORAGE_KEYS.cfg);
|
const raw = localStorage.getItem(STORAGE_KEYS.cfg);
|
||||||
if (!raw) return structuredClone(DEFAULT_CONFIG);
|
if (!raw) return structuredClone(DEFAULT_CONFIG);
|
||||||
const saved = JSON.parse(raw);
|
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 || {}) } };
|
return { ...DEFAULT_CONFIG, ...saved, RUNTIME: { ...DEFAULT_CONFIG.RUNTIME, ...(saved.RUNTIME || {}) } };
|
||||||
} catch {
|
} catch {
|
||||||
return structuredClone(DEFAULT_CONFIG);
|
return structuredClone(DEFAULT_CONFIG);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function saveConfig(cfg) {
|
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();
|
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;
|
||||||
|
|
||||||
|
|
@ -516,6 +517,7 @@
|
||||||
|
|
||||||
const dump = JSON.parse(JSON.stringify(this.cfg));
|
const dump = JSON.parse(JSON.stringify(this.cfg));
|
||||||
if (dump.BRIDGE_KEY) dump.BRIDGE_KEY = '•'.repeat(8);
|
if (dump.BRIDGE_KEY) dump.BRIDGE_KEY = '•'.repeat(8);
|
||||||
|
dump.VERSION = DEFAULT_CONFIG.VERSION;
|
||||||
root.querySelector('.rc-json').value = JSON.stringify(dump, null, 2);
|
root.querySelector('.rc-json').value = JSON.stringify(dump, null, 2);
|
||||||
|
|
||||||
const bridgeKeyInput = root.querySelector('.rc-bridge-key');
|
const bridgeKeyInput = root.querySelector('.rc-bridge-key');
|
||||||
|
|
@ -615,6 +617,10 @@
|
||||||
delete parsed.BRIDGE_KEY;
|
delete parsed.BRIDGE_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent overriding ephemeral fields from pasted JSON
|
||||||
|
delete parsed.VERSION;
|
||||||
|
delete parsed.RUNTIME;
|
||||||
|
|
||||||
Object.assign(this.cfg, parsed);
|
Object.assign(this.cfg, parsed);
|
||||||
saveConfig(this.cfg);
|
saveConfig(this.cfg);
|
||||||
|
|
||||||
|
|
@ -825,13 +831,32 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function findAllCommandsInMessage(el) {
|
function findAllCommandsInMessage(el) {
|
||||||
const blocks = el.querySelectorAll('pre code, pre, code');
|
|
||||||
const hits = [];
|
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) {
|
for (const b of blocks) {
|
||||||
const txt = (b.textContent || '').trim();
|
const txt = (b.textContent || '').trim();
|
||||||
const parts = extractAllCompleteBlocks(txt);
|
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;
|
return hits;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -943,29 +968,39 @@
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const el = getVisibleInputCandidate();
|
const el = getVisibleInputCandidate();
|
||||||
|
// Pre-paste: the send button can be disabled. Don't block on it here.
|
||||||
const btn = findSendButton(el);
|
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');
|
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;
|
let hasUnsent = false;
|
||||||
if (el) {
|
if (el) {
|
||||||
try {
|
try {
|
||||||
const currentText = (el.textContent || el.value || '').trim();
|
const currentText = (el.textContent || el.value || '').trim();
|
||||||
if (currentText.startsWith('@bridge@') || currentText.startsWith('### [')) {
|
hasUnsent = currentText.length > 0 && !/^\s*$/.test(currentText);
|
||||||
hasUnsent = true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
RC_DEBUG?.verbose('Failed to check composer content', { error: String(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);
|
await ExecutionManager.delay(pollMs);
|
||||||
}
|
}
|
||||||
RC_DEBUG?.warn('Composer not ready within timeout');
|
RC_DEBUG?.warn('Composer not ready within timeout');
|
||||||
|
|
@ -1387,12 +1422,24 @@
|
||||||
// ---------------------- Paste + Submit helpers ----------------------
|
// ---------------------- Paste + Submit helpers ----------------------
|
||||||
function getVisibleInputCandidate() {
|
function getVisibleInputCandidate() {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
'.ProseMirror#prompt-textarea',
|
// ChatGPT / GPT
|
||||||
'#prompt-textarea.ProseMirror',
|
|
||||||
'#prompt-textarea',
|
'#prompt-textarea',
|
||||||
'.ProseMirror',
|
'.ProseMirror#prompt-textarea',
|
||||||
'[contenteditable="true"]',
|
'.ProseMirror[role="textbox"][contenteditable="true"]',
|
||||||
'textarea'
|
'[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) {
|
for (const sel of candidates) {
|
||||||
const el = document.querySelector(sel);
|
const el = document.querySelector(sel);
|
||||||
|
|
@ -1406,24 +1453,98 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function findSendButton(scopeEl) {
|
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 = [
|
const selectors = [
|
||||||
'button[data-testid="send-button"]',
|
'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*="send"]',
|
'button[aria-label*="send"]',
|
||||||
'button[aria-label*="Submit"]',
|
'button[aria-label*="Submit"]',
|
||||||
'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) {
|
for (const s of selectors) {
|
||||||
const btn = scope.querySelector(s);
|
const btn = scope.querySelector(s);
|
||||||
if (!btn) continue;
|
if (btn) {
|
||||||
const style = window.getComputedStyle(btn);
|
const style = window.getComputedStyle(btn);
|
||||||
const disabled = btn.disabled || btn.getAttribute('aria-disabled') === 'true';
|
const disabled = btn.disabled || btn.getAttribute('aria-disabled') === 'true';
|
||||||
if (style.display === 'none' || style.visibility === 'hidden') continue;
|
const hidden = style.display === 'none' || style.visibility === 'hidden';
|
||||||
if (btn.offsetParent === null && style.position !== 'fixed') continue;
|
const notRendered = btn.offsetParent === null && style.position !== 'fixed';
|
||||||
if (!disabled) return btn;
|
|
||||||
|
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;
|
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 ----------------------
|
// ---------------------- Execution ----------------------
|
||||||
class ExecutionManager {
|
class ExecutionManager {
|
||||||
static async executeCommand(command, sourceElement, renderKey = '', label = '') {
|
static async executeCommand(command, sourceElement, renderKey = '', label = '') {
|
||||||
|
|
@ -1918,7 +2147,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) {
|
||||||
(window.AI_REPO_RESPONSES || new ResponseBuffer()).push({ label, content: body });
|
window.AI_REPO_RESPONSES.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 });
|
||||||
}
|
}
|
||||||
|
|
@ -1928,10 +2157,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);
|
||||||
(window.AI_REPO_RESPONSES || new ResponseBuffer()).push({ label, content: listing });
|
window.AI_REPO_RESPONSES.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```';
|
||||||
(window.AI_REPO_RESPONSES || new ResponseBuffer()).push({ label, content: fallback });
|
window.AI_REPO_RESPONSES.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.',
|
||||||
|
|
@ -2020,114 +2249,6 @@
|
||||||
cancelOne: (cb) => execQueue.cancelOne(cb),
|
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 ----------------------
|
// ---------------------- Bridge Key ----------------------
|
||||||
let BRIDGE_KEY = null;
|
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) ----------------------
|
// ---------------------- Monitor (with streaming "settle" & complete-block check) ----------------------
|
||||||
class CommandMonitor {
|
class CommandMonitor {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -2375,18 +2527,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
findCommandInCodeBlock(el) {
|
findCommandInCodeBlock(el) {
|
||||||
const blocks = el.querySelectorAll('pre code, pre, code');
|
return findCommandTextInElement(el);
|
||||||
// 🔍 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scanMessages() {
|
scanMessages() {
|
||||||
|
|
@ -2937,7 +3078,7 @@
|
||||||
// ---------------------- Test commands ----------------------
|
// ---------------------- Test commands ----------------------
|
||||||
const TEST_COMMANDS = {
|
const TEST_COMMANDS = {
|
||||||
validUpdate:
|
validUpdate:
|
||||||
`\
|
`\
|
||||||
\`\`\`yaml
|
\`\`\`yaml
|
||||||
@bridge@
|
@bridge@
|
||||||
action: update_file
|
action: update_file
|
||||||
|
|
@ -2952,7 +3093,7 @@ content: |
|
||||||
\`\`\`
|
\`\`\`
|
||||||
`,
|
`,
|
||||||
getFile:
|
getFile:
|
||||||
`\
|
`\
|
||||||
\`\`\`yaml
|
\`\`\`yaml
|
||||||
@bridge@
|
@bridge@
|
||||||
action: get_file
|
action: get_file
|
||||||
|
|
@ -2962,7 +3103,7 @@ path: README.md
|
||||||
\`\`\`
|
\`\`\`
|
||||||
`,
|
`,
|
||||||
listFiles:
|
listFiles:
|
||||||
`\
|
`\
|
||||||
\`\`\`yaml
|
\`\`\`yaml
|
||||||
@bridge@
|
@bridge@
|
||||||
action: list_files
|
action: list_files
|
||||||
|
|
@ -2972,7 +3113,7 @@ path: .
|
||||||
\`\`\`
|
\`\`\`
|
||||||
`,
|
`,
|
||||||
createBranch:
|
createBranch:
|
||||||
`\
|
`\
|
||||||
\`\`\`yaml
|
\`\`\`yaml
|
||||||
@bridge@
|
@bridge@
|
||||||
action: create_branch
|
action: create_branch
|
||||||
|
|
@ -2983,7 +3124,7 @@ source_branch: main
|
||||||
\`\`\`
|
\`\`\`
|
||||||
`,
|
`,
|
||||||
createPR:
|
createPR:
|
||||||
`\
|
`\
|
||||||
\`\`\`yaml
|
\`\`\`yaml
|
||||||
@bridge@
|
@bridge@
|
||||||
action: create_pr
|
action: create_pr
|
||||||
|
|
@ -2999,7 +3140,7 @@ body: |
|
||||||
\`\`\`
|
\`\`\`
|
||||||
`,
|
`,
|
||||||
createIssue:
|
createIssue:
|
||||||
`\
|
`\
|
||||||
\`\`\`yaml
|
\`\`\`yaml
|
||||||
@bridge@
|
@bridge@
|
||||||
action: create_issue
|
action: create_issue
|
||||||
|
|
@ -3014,7 +3155,7 @@ body: |
|
||||||
\`\`\`
|
\`\`\`
|
||||||
`,
|
`,
|
||||||
createTag:
|
createTag:
|
||||||
`\
|
`\
|
||||||
\`\`\`yaml
|
\`\`\`yaml
|
||||||
@bridge@
|
@bridge@
|
||||||
action: create_tag
|
action: create_tag
|
||||||
|
|
@ -3026,7 +3167,7 @@ message: Release version 1.0.0
|
||||||
\`\`\`
|
\`\`\`
|
||||||
`,
|
`,
|
||||||
createRelease:
|
createRelease:
|
||||||
`\
|
`\
|
||||||
\`\`\`yaml
|
\`\`\`yaml
|
||||||
@bridge@
|
@bridge@
|
||||||
action: create_release
|
action: create_release
|
||||||
|
|
@ -3045,7 +3186,7 @@ body: |
|
||||||
\`\`\`
|
\`\`\`
|
||||||
`,
|
`,
|
||||||
multiCommand:
|
multiCommand:
|
||||||
`\
|
`\
|
||||||
\`\`\`yaml
|
\`\`\`yaml
|
||||||
@bridge@
|
@bridge@
|
||||||
action: get_file
|
action: get_file
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue