included more features
This commit is contained in:
parent
52559be1c5
commit
aac79a689f
|
|
@ -0,0 +1,70 @@
|
||||||
|
// ==DEBUG PANEL START==
|
||||||
|
// Depends on: config.js, logger.js, queue.js
|
||||||
|
(function () {
|
||||||
|
const cfg = () => window.AI_REPO_CONFIG;
|
||||||
|
const log = () => window.AI_REPO_LOGGER;
|
||||||
|
|
||||||
|
class DebugPanel {
|
||||||
|
constructor() { this.root = null; }
|
||||||
|
mount() {
|
||||||
|
if (!cfg().get('debug.showPanel')) return;
|
||||||
|
if (this.root) return;
|
||||||
|
const root = document.createElement('div');
|
||||||
|
root.style.cssText = `
|
||||||
|
position:fixed; right:16px; bottom:16px; z-index:2147483647; width:460px; max-height:55vh;
|
||||||
|
display:flex; flex-direction:column; background:rgba(20,20,24,.92); border:1px solid #3b3b46; border-radius:8px;
|
||||||
|
color:#e5e7eb; font:12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; box-shadow:0 16px 40px rgba(0,0,0,.55);
|
||||||
|
`;
|
||||||
|
root.innerHTML = `
|
||||||
|
<div style="display:flex;gap:8px;align-items:center;padding:8px;border-bottom:1px solid #2c2c33">
|
||||||
|
<strong style="flex:1">AI Repo Commander</strong>
|
||||||
|
<button data-act="copy" style="padding:4px 6px;">Copy logs</button>
|
||||||
|
<button data-act="pause" style="padding:4px 6px;">${cfg().get('runtime.paused')?'Resume':'Pause'}</button>
|
||||||
|
<button data-act="clearq" style="padding:4px 6px;background:#7c2d12;color:#fff;border:1px solid #991b1b">Clear Queue</button>
|
||||||
|
</div>
|
||||||
|
<div data-body style="overflow:auto;padding:8px;flex:1"></div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(root);
|
||||||
|
this.root = root;
|
||||||
|
this.body = root.querySelector('[data-body]');
|
||||||
|
this._wire();
|
||||||
|
this._tick();
|
||||||
|
}
|
||||||
|
_wire() {
|
||||||
|
this.root.addEventListener('click', (e) => {
|
||||||
|
const btn = e.target.closest('button[data-act]'); if (!btn) return;
|
||||||
|
const act = btn.getAttribute('data-act');
|
||||||
|
if (act === 'copy') navigator.clipboard?.writeText(log().getRecentLogs(200));
|
||||||
|
if (act === 'pause') {
|
||||||
|
const paused = !cfg().get('runtime.paused'); cfg().set('runtime.paused', paused);
|
||||||
|
btn.textContent = paused ? 'Resume' : 'Pause';
|
||||||
|
log().info(paused ? 'Paused' : 'Resumed');
|
||||||
|
}
|
||||||
|
if (act === 'clearq') window.AI_REPO_QUEUE.clear();
|
||||||
|
});
|
||||||
|
// queue badge
|
||||||
|
const q = window.AI_REPO_QUEUE;
|
||||||
|
if (q) q.onSizeChange = (n) => this._toast(`Queue: ${n}`);
|
||||||
|
}
|
||||||
|
_toast(msg) {
|
||||||
|
if (!this.root) return;
|
||||||
|
const t = document.createElement('div');
|
||||||
|
t.textContent = msg;
|
||||||
|
t.style.cssText = 'position:absolute; right:12px; bottom:12px; padding:6px 10px; background:#111827; color:#e5e7eb; border:1px solid #374151; border-radius:6px;';
|
||||||
|
this.root.appendChild(t); setTimeout(() => t.remove(), 1000);
|
||||||
|
}
|
||||||
|
_tick() {
|
||||||
|
if (!this.body) return;
|
||||||
|
const rows = window.AI_REPO_LOGGER?.buffer?.slice(-80) || [];
|
||||||
|
this.body.innerHTML = rows.map(e => `<div style="padding:2px 0;border-bottom:1px dashed #2a2a34;">${e.timestamp} ${e.level} ${e.message}</div>`).join('');
|
||||||
|
requestAnimationFrame(() => this._tick());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const panel = new DebugPanel();
|
||||||
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => panel.mount());
|
||||||
|
else panel.mount();
|
||||||
|
|
||||||
|
window.AI_REPO_DEBUG_PANEL = panel;
|
||||||
|
})();
|
||||||
|
// ==DEBUG PANEL END==
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
// ==DETECTOR START==
|
||||||
|
// Depends on: config.js, logger.js, queue.js, command-parser.js, command-executor.js, storage.js
|
||||||
|
(function () {
|
||||||
|
const cfg = () => window.AI_REPO_CONFIG;
|
||||||
|
const log = () => window.AI_REPO_LOGGER;
|
||||||
|
|
||||||
|
function extractAllBlocks(text) {
|
||||||
|
const out = []; const re = /^\s*@bridge@[ \t]*\n([\s\S]*?)\n@end@[ \t]*(?:\n|$)/gm;
|
||||||
|
let m; while ((m = re.exec(text)) !== null) out.push(m[0]);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAssistantMsg(el) {
|
||||||
|
const sels = [
|
||||||
|
'[data-message-author-role="assistant"]',
|
||||||
|
'.chat-message:not([data-message-author-role="user"])',
|
||||||
|
'.message-content'
|
||||||
|
];
|
||||||
|
return sels.some(s => el.matches?.(s) || el.querySelector?.(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function settleText(el, initial, windowMs, pollMs) {
|
||||||
|
let deadline = Date.now() + windowMs;
|
||||||
|
let last = initial;
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
await new Promise(r => setTimeout(r, pollMs));
|
||||||
|
const fresh = el.textContent || '';
|
||||||
|
const blocks = extractAllBlocks(fresh);
|
||||||
|
const pick = blocks.join('\n'); // if multiple, concatenate for stability check (we’ll split later)
|
||||||
|
if (pick === last && pick) continue; // stable—keep waiting out the window
|
||||||
|
if (pick && pick !== last) { last = pick; deadline = Date.now() + windowMs; }
|
||||||
|
}
|
||||||
|
return last;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Detector {
|
||||||
|
constructor() {
|
||||||
|
this.observer = null;
|
||||||
|
this.processed = new WeakSet();
|
||||||
|
this.clusterLookahead = 3;
|
||||||
|
this.clusterWindowMs = 1000;
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
this.observer = new MutationObserver((mutations) => {
|
||||||
|
if (cfg().get('runtime.paused')) return;
|
||||||
|
let should = false;
|
||||||
|
for (const m of mutations) {
|
||||||
|
if (m.type === 'childList') {
|
||||||
|
for (const n of m.addedNodes) {
|
||||||
|
if (n.nodeType === 1 && isAssistantMsg(n)) { this._handle(n); should = true; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m.type === 'characterData') {
|
||||||
|
const el = m.target?.parentElement;
|
||||||
|
if (el && isAssistantMsg(el)) should = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (should) {/* no-op: _handle already queued */}
|
||||||
|
});
|
||||||
|
this.observer.observe(document.body, { childList: true, subtree: true, characterData: true, attributes: true });
|
||||||
|
if (cfg().get('ui.processExisting')) {
|
||||||
|
document.querySelectorAll('[data-message-author-role], .chat-message, .message-content')
|
||||||
|
.forEach(el => isAssistantMsg(el) && this._handle(el));
|
||||||
|
}
|
||||||
|
log().info('Detector started');
|
||||||
|
}
|
||||||
|
|
||||||
|
async _handle(el) {
|
||||||
|
if (this.processed.has(el)) return;
|
||||||
|
this.processed.add(el);
|
||||||
|
|
||||||
|
// Debounce complete generation
|
||||||
|
const debounce = cfg().get('execution.debounceDelay') || 0;
|
||||||
|
if (debounce > 0) await new Promise(r => setTimeout(r, debounce));
|
||||||
|
|
||||||
|
// Settle
|
||||||
|
const baseText = el.textContent || '';
|
||||||
|
const stable = await settleText(el, baseText, cfg().get('execution.settleCheckMs') || 1200, cfg().get('execution.settlePollMs') || 250);
|
||||||
|
const blocks = extractAllBlocks(stable);
|
||||||
|
if (!blocks.length) { this.processed.delete(el); return; } // not a command after all
|
||||||
|
|
||||||
|
const maxPerMsg = cfg().get('queue.maxPerMessage') || 5;
|
||||||
|
blocks.slice(0, maxPerMsg).forEach((cmdText, idx) => this._enqueueOne(el, cmdText, idx));
|
||||||
|
|
||||||
|
// Cluster rescan: look ahead a few assistant messages for chained blocks
|
||||||
|
setTimeout(() => this._clusterRescan(el), this.clusterWindowMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
_enqueueOne(el, commandText, idx) {
|
||||||
|
const history = window.AI_REPO_HISTORY;
|
||||||
|
if (history.isProcessed(el, idx)) {
|
||||||
|
this._addRunAgain(el, commandText, idx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
history.markProcessed(el, idx);
|
||||||
|
|
||||||
|
window.AI_REPO_QUEUE.push(async () => {
|
||||||
|
try {
|
||||||
|
const parsed = window.AI_REPO_PARSER.parse(commandText);
|
||||||
|
const v = window.AI_REPO_PARSER.validate(parsed);
|
||||||
|
if (!v.isValid) throw new Error(`Validation failed: ${v.errors.join(', ')}`);
|
||||||
|
if (v.example) { log().info('Example command skipped'); return; }
|
||||||
|
await window.AI_REPO_EXECUTOR.execute(parsed, el, `[${idx + 1}] ${parsed.action}`);
|
||||||
|
} catch (e) {
|
||||||
|
log().error('Command failed', { error: e.message });
|
||||||
|
this._addRunAgain(el, commandText, idx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_addRunAgain(el, commandText, idx) {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.textContent = `Run Again #${idx + 1}`;
|
||||||
|
btn.style.cssText = 'padding:4px 8px;margin:4px;border:1px solid #374151;border-radius:4px;background:#1f2937;color:#e5e7eb;cursor:pointer;';
|
||||||
|
btn.addEventListener('click', () => this._enqueueOne(el, commandText, idx));
|
||||||
|
el.appendChild(btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
_clusterRescan(anchor) {
|
||||||
|
let scanned = 0; let cur = anchor.nextElementSibling;
|
||||||
|
while (cur && scanned < this.clusterLookahead) {
|
||||||
|
if (!isAssistantMsg(cur)) break;
|
||||||
|
if (!this.processed.has(cur)) this._handle(cur);
|
||||||
|
scanned++; cur = cur.nextElementSibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.AI_REPO_DETECTOR = new Detector();
|
||||||
|
})();
|
||||||
|
// ==DETECTOR END==
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
// ==FINGERPRINT (drop-in utility) ==
|
||||||
|
(function(){
|
||||||
|
function norm(s){ return (s||'').replace(/\r/g,'').replace(/\u200b/g,'').replace(/[ \t]+\n/g,'\n').trim(); }
|
||||||
|
function hash(s){ let h=5381; for(let i=0;i<s.length;i++) h=((h<<5)+h)^s.charCodeAt(i); return (h>>>0).toString(36); }
|
||||||
|
function commandLikeText(el){
|
||||||
|
const blocks = el.querySelectorAll('pre code, pre, code');
|
||||||
|
for (const b of blocks) {
|
||||||
|
const t = norm(b.textContent || '');
|
||||||
|
if (/@end@\s*$/m.test(t) && /(^|\n)\s*@bridge@\b/m.test(t) && /(^|\n)\s*action\s*:/m.test(t)) return t;
|
||||||
|
}
|
||||||
|
return norm((el.textContent || '').slice(0, 2000));
|
||||||
|
}
|
||||||
|
function prevContextHash(el) {
|
||||||
|
const list = Array.from(document.querySelectorAll('[data-message-author-role], .chat-message, .message-content'));
|
||||||
|
const idx = list.indexOf(el); if (idx <= 0) return '0';
|
||||||
|
let rem = 2000, buf = '';
|
||||||
|
for (let i=idx-1; i>=0 && rem>0; i--){
|
||||||
|
const t = norm(list[i].textContent || ''); if (!t) continue;
|
||||||
|
const take = t.slice(-rem); buf = take + buf; rem -= take.length;
|
||||||
|
}
|
||||||
|
return hash(buf.slice(-2000));
|
||||||
|
}
|
||||||
|
function intraPrefixHash(el){
|
||||||
|
const t = el.textContent || '';
|
||||||
|
const m = t.match(/@bridge@[\s\S]*?@end@/m);
|
||||||
|
const endIdx = m ? t.indexOf(m[0]) : t.length;
|
||||||
|
return hash(norm(t.slice(Math.max(0, endIdx - 2000), endIdx)));
|
||||||
|
}
|
||||||
|
window.AI_REPO_FINGERPRINT = function(el){
|
||||||
|
const ch = hash(commandLikeText(el).slice(0, 2000));
|
||||||
|
const ph = prevContextHash(el);
|
||||||
|
const ih = intraPrefixHash(el);
|
||||||
|
return `ch:${ch}|ph:${ph}|ih:${ih}`;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
@ -154,6 +154,8 @@
|
||||||
} else {
|
} else {
|
||||||
window.AI_REPO_MAIN = new AIRepoCommander();
|
window.AI_REPO_MAIN = new AIRepoCommander();
|
||||||
window.AI_REPO_MAIN.initialize();
|
window.AI_REPO_MAIN.initialize();
|
||||||
|
// Kick off the advanced detector (restores settle/debounce, multi-block, cluster rescan)
|
||||||
|
window.AI_REPO_DETECTOR?.start();
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
// ==MAIN END==
|
// ==MAIN END==
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
// ==PASTE SUBMIT START==
|
||||||
|
// Depends on: config.js, logger.js
|
||||||
|
/* global GM_setClipboard */
|
||||||
|
(function () {
|
||||||
|
const cfg = () => window.AI_REPO_CONFIG;
|
||||||
|
const log = () => window.AI_REPO_LOGGER;
|
||||||
|
|
||||||
|
function findComposer() {
|
||||||
|
const sels = [
|
||||||
|
'#prompt-textarea',
|
||||||
|
'.ProseMirror#prompt-textarea',
|
||||||
|
'.ProseMirror[role="textbox"][contenteditable="true"]',
|
||||||
|
'[data-testid="composer"] [contenteditable="true"][role="textbox"]',
|
||||||
|
'main [contenteditable="true"][role="textbox"]',
|
||||||
|
'textarea[data-testid="input-area"]',
|
||||||
|
'[contenteditable="true"][aria-label*="Message"]',
|
||||||
|
'textarea',
|
||||||
|
'[contenteditable="true"]'
|
||||||
|
];
|
||||||
|
for (const s of sels) {
|
||||||
|
const el = document.querySelector(s);
|
||||||
|
if (!el) continue;
|
||||||
|
const st = window.getComputedStyle(el);
|
||||||
|
if (st.display === 'none' || st.visibility === 'hidden') continue;
|
||||||
|
if (el.offsetParent === null && st.position !== 'fixed') continue;
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findSendButton(scopeEl) {
|
||||||
|
const scope = scopeEl?.closest('form, [data-testid="composer"], main, body') || document;
|
||||||
|
const sels = [
|
||||||
|
'button[data-testid="send-button"]',
|
||||||
|
'#composer-submit-button',
|
||||||
|
'button[aria-label*="Send prompt"]',
|
||||||
|
'button[aria-label*="Send message"]',
|
||||||
|
'button[aria-label="Send"]',
|
||||||
|
'button[aria-label*="Send"]',
|
||||||
|
'form button'
|
||||||
|
];
|
||||||
|
for (const s of sels) {
|
||||||
|
const b = scope.querySelector(s) || document.querySelector(s);
|
||||||
|
if (!b) continue;
|
||||||
|
const st = window.getComputedStyle(b);
|
||||||
|
const disabled = b.disabled || b.getAttribute('aria-disabled') === 'true';
|
||||||
|
const hidden = st.display === 'none' || st.visibility === 'hidden';
|
||||||
|
const notRendered = b.offsetParent === null && st.position !== 'fixed';
|
||||||
|
if (!disabled && !hidden && !notRendered) return b;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pressEnter(el) {
|
||||||
|
for (const t of ['keydown','keypress','keyup']) {
|
||||||
|
const ok = el.dispatchEvent(new KeyboardEvent(t, { key:'Enter', code:'Enter', keyCode:13, which:13, bubbles:true, cancelable:true }));
|
||||||
|
if (!ok) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitReady(timeoutMs = 12000) {
|
||||||
|
const start = Date.now();
|
||||||
|
while (Date.now() - start < timeoutMs) {
|
||||||
|
const el = findComposer();
|
||||||
|
if (el) {
|
||||||
|
const current = (el.textContent || el.value || '').trim();
|
||||||
|
const busy = el.closest('form, [data-testid="composer"], main, body')?.querySelector('[aria-busy="true"], [data-state="loading"], .typing-indicator');
|
||||||
|
if (!busy && current.length === 0) return true;
|
||||||
|
}
|
||||||
|
await new Promise(r => setTimeout(r, 200));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pasteInto(el, text) {
|
||||||
|
const payload = cfg().get('ui.appendTrailingNewline') ? (text.endsWith('\n') ? text : text + '\n') : text;
|
||||||
|
try {
|
||||||
|
// ClipboardEvent path
|
||||||
|
const dt = new DataTransfer(); dt.setData('text/plain', payload);
|
||||||
|
const evt = new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true });
|
||||||
|
if (el.dispatchEvent(evt) && !evt.defaultPrevented) return true;
|
||||||
|
} catch {}
|
||||||
|
// ProseMirror path
|
||||||
|
if (el.classList?.contains('ProseMirror')) {
|
||||||
|
const node = document.createTextNode('\n' + payload.replace(/\n?$/,'\n') + '\n');
|
||||||
|
el.innerHTML = ''; el.appendChild(node);
|
||||||
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Selection/contentEditable path
|
||||||
|
try {
|
||||||
|
if (el.isContentEditable || el.getAttribute('contenteditable') === 'true') {
|
||||||
|
const sel = window.getSelection(); if (sel && sel.rangeCount === 0) {
|
||||||
|
const r = document.createRange(); r.selectNodeContents(el); r.collapse(false); sel.removeAllRanges(); sel.addRange(r);
|
||||||
|
}
|
||||||
|
const range = window.getSelection()?.getRangeAt(0);
|
||||||
|
if (range) {
|
||||||
|
range.deleteContents(); const node = document.createTextNode(payload);
|
||||||
|
range.insertNode(node); range.setStartAfter(node); range.setEndAfter(node);
|
||||||
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
// Textarea path
|
||||||
|
if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
|
||||||
|
el.value = payload; el.dispatchEvent(new Event('input', { bubbles: true })); return true;
|
||||||
|
}
|
||||||
|
// Fallback to clipboard
|
||||||
|
try {
|
||||||
|
if (typeof GM_setClipboard === 'function') {
|
||||||
|
GM_setClipboard(payload, { type:'text', mimetype:'text/plain' });
|
||||||
|
alert('Content copied to clipboard. Press Ctrl/Cmd+V to paste.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitToComposer(text) {
|
||||||
|
const auto = !!cfg().get('ui.autoSubmit');
|
||||||
|
const ok = await waitReady(cfg().get('execution.settleCheckMs') || 1200);
|
||||||
|
if (!ok) { log()?.warn('Composer not ready'); return false; }
|
||||||
|
|
||||||
|
const el = findComposer(); if (!el) { log()?.warn('Composer not found'); return false; }
|
||||||
|
if (text && !pasteInto(el, text)) { log()?.warn('Paste failed'); return false; }
|
||||||
|
|
||||||
|
await new Promise(r => setTimeout(r, cfg().get('ui.postPasteDelayMs') || 600));
|
||||||
|
if (!auto) return true;
|
||||||
|
|
||||||
|
const btn = findSendButton(el);
|
||||||
|
if (btn) { btn.click(); return true; }
|
||||||
|
return pressEnter(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.AI_REPO_PASTE = { submitToComposer };
|
||||||
|
})();
|
||||||
|
// ==PASTE SUBMIT END==
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
// ==QUEUE START==
|
||||||
|
// Depends on: config.js, logger.js
|
||||||
|
(function () {
|
||||||
|
class ExecutionQueue {
|
||||||
|
constructor(opts = {}) {
|
||||||
|
const cfg = window.AI_REPO_CONFIG;
|
||||||
|
this.minDelayMs = opts.minDelayMs ?? cfg.get('queue.minDelayMs') ?? 1500;
|
||||||
|
this.maxPerMinute = opts.maxPerMinute ?? cfg.get('queue.maxPerMinute') ?? 15;
|
||||||
|
this.q = [];
|
||||||
|
this.running = false;
|
||||||
|
this.timestamps = [];
|
||||||
|
this.onSizeChange = null;
|
||||||
|
}
|
||||||
|
push(task) {
|
||||||
|
this.q.push(task);
|
||||||
|
this.onSizeChange?.(this.q.length);
|
||||||
|
if (!this.running) void this._drain();
|
||||||
|
}
|
||||||
|
clear() { this.q.length = 0; this.onSizeChange?.(0); }
|
||||||
|
size() { return this.q.length; }
|
||||||
|
_withinBudget() {
|
||||||
|
const now = Date.now();
|
||||||
|
this.timestamps = this.timestamps.filter(t => now - t < 60_000);
|
||||||
|
return this.timestamps.length < this.maxPerMinute;
|
||||||
|
}
|
||||||
|
async _drain() {
|
||||||
|
if (this.running) return;
|
||||||
|
this.running = true;
|
||||||
|
while (this.q.length) {
|
||||||
|
while (!this._withinBudget()) await this._delay(400);
|
||||||
|
const fn = this.q.shift();
|
||||||
|
this.onSizeChange?.(this.q.length);
|
||||||
|
try { await fn(); } catch (e) { window.AI_REPO_LOGGER?.warn('Queue task error', { error: String(e) }); }
|
||||||
|
this.timestamps.push(Date.now());
|
||||||
|
await this._delay(this.minDelayMs);
|
||||||
|
}
|
||||||
|
this.running = false;
|
||||||
|
}
|
||||||
|
_delay(ms){ return new Promise(r => setTimeout(r, ms)); }
|
||||||
|
}
|
||||||
|
window.AI_REPO_QUEUE = new ExecutionQueue();
|
||||||
|
})();
|
||||||
|
// ==QUEUE END==
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
// ==RESPONSE BUFFER START==
|
||||||
|
// Depends on: config.js, logger.js, queue.js, paste-submit.js
|
||||||
|
(function () {
|
||||||
|
function chunkByLines(s, limit) {
|
||||||
|
const out = []; let start = 0;
|
||||||
|
while (start < s.length) {
|
||||||
|
const soft = s.lastIndexOf('\n', Math.min(start + limit, s.length));
|
||||||
|
const end = soft > start ? soft + 1 : Math.min(start + limit, s.length);
|
||||||
|
out.push(s.slice(start, end)); start = end;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function isSingleFence(s){ return /^```[^\n]*\n[\s\S]*\n```$/.test(s.trim()); }
|
||||||
|
function splitRespectingFence(text, limit) {
|
||||||
|
const t = text.trim(); if (!isSingleFence(t)) return chunkByLines(text, limit);
|
||||||
|
const m = /^```([^\n]*)\n([\s\S]*)\n```$/.exec(t);
|
||||||
|
const lang = (m?.[1] || 'text').trim(); const inner = m?.[2] ?? '';
|
||||||
|
const chunks = chunkByLines(inner, limit - 16 - lang.length);
|
||||||
|
return chunks.map(c => '```' + lang + '\n' + c.replace(/\n?$/, '\n') + '```');
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResponseBuffer {
|
||||||
|
constructor() {
|
||||||
|
this.pending = []; this.timer = null; this.flushing = false;
|
||||||
|
}
|
||||||
|
push({ label, content }) {
|
||||||
|
if (!content) return;
|
||||||
|
this.pending.push({ label, content });
|
||||||
|
this._schedule();
|
||||||
|
}
|
||||||
|
_schedule() {
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
this.timer = setTimeout(() => this.flush(), 500);
|
||||||
|
}
|
||||||
|
_build() {
|
||||||
|
const showHeadings = true; // readable by default
|
||||||
|
const parts = [];
|
||||||
|
for (const { label, content } of this.pending) {
|
||||||
|
if (showHeadings && label) parts.push(`### ${label}\n`);
|
||||||
|
parts.push(String(content).trimEnd(), '');
|
||||||
|
}
|
||||||
|
return parts.join('\n');
|
||||||
|
}
|
||||||
|
async flush() {
|
||||||
|
if (this.flushing || !this.pending.length) return;
|
||||||
|
this.flushing = true;
|
||||||
|
const toPaste = this._build(); this.pending.length = 0;
|
||||||
|
try {
|
||||||
|
const limit = 250_000;
|
||||||
|
if (toPaste.length > limit) {
|
||||||
|
const chunks = splitRespectingFence(toPaste, limit);
|
||||||
|
chunks.forEach((c, i) => {
|
||||||
|
const header = `### Part ${i+1}/${chunks.length}\n`;
|
||||||
|
const payload = header + c;
|
||||||
|
window.AI_REPO_QUEUE.push(async () => {
|
||||||
|
await window.AI_REPO_PASTE.submitToComposer(payload);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.AI_REPO_QUEUE.push(async () => {
|
||||||
|
await window.AI_REPO_PASTE.submitToComposer(toPaste);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.flushing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.AI_REPO_RESPONSES = new ResponseBuffer();
|
||||||
|
})();
|
||||||
|
// ==RESPONSE BUFFER END==
|
||||||
|
|
@ -34,11 +34,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
_fingerprint(el, idx) {
|
_fingerprint(el, idx) {
|
||||||
const text = (el.textContent || '').slice(0, 1000);
|
const base = window.AI_REPO_FINGERPRINT ? window.AI_REPO_FINGERPRINT(el) : this._hash((el.textContent||'').slice(0,1000));
|
||||||
const list = Array.from(document.querySelectorAll('[data-message-author-role], .chat-message, .message-content'));
|
return `${base}|idx:${idx}`;
|
||||||
const pos = list.indexOf(el);
|
|
||||||
return `conv:${this.conversationId}|pos:${pos}|idx:${idx}|hash:${this._hash(text)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_hash(str) {
|
_hash(str) {
|
||||||
let h = 5381;
|
let h = 5381;
|
||||||
for (let i = 0; i < Math.min(str.length, 1000); i++) h = ((h << 5) + h) ^ str.charCodeAt(i);
|
for (let i = 0; i < Math.min(str.length, 1000); i++) h = ((h << 5) + h) ^ str.charCodeAt(i);
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name AI Repo Commander (Modular)
|
// @name AI Repo Commander (Full Features)
|
||||||
// @namespace http://tampermonkey.net/
|
// @namespace http://tampermonkey.net/
|
||||||
// @version 2.0.0
|
// @version 2.1.0
|
||||||
// @description Modularized AI Repo Commander
|
// @description Full modular AI Repo Commander with all features
|
||||||
|
// @author Robert Dickson
|
||||||
// @match https://chat.openai.com/*
|
// @match https://chat.openai.com/*
|
||||||
// @match https://chatgpt.com/*
|
// @match https://chatgpt.com/*
|
||||||
// @match https://claude.ai/*
|
// @match https://claude.ai/*
|
||||||
// @match https://gemini.google.com/*
|
// @match https://gemini.google.com/*
|
||||||
// @grant GM_xmlhttpRequest
|
// @grant GM_xmlhttpRequest
|
||||||
|
// @grant GM_setClipboard
|
||||||
// @connect n8n.brrd.tech
|
// @connect n8n.brrd.tech
|
||||||
// @connect *
|
// @connect *
|
||||||
// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/config.js
|
// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/config.js
|
||||||
|
|
@ -16,4 +18,9 @@
|
||||||
// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/command-parser.js
|
// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/command-parser.js
|
||||||
// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/command-executor.js
|
// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/command-executor.js
|
||||||
// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/main.js
|
// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/main.js
|
||||||
|
// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/queue.js
|
||||||
|
// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/response-buffer.js
|
||||||
|
// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/paste-submit.js
|
||||||
|
// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/detector.js
|
||||||
|
// @require https://gitea.brrd.tech/rob/AI-Repo-Commander/raw/branch/refactor-structure/src/debug-panel.js
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
Loading…
Reference in New Issue