included more features

This commit is contained in:
rob 2025-10-15 20:42:31 -03:00
parent 52559be1c5
commit aac79a689f
9 changed files with 505 additions and 8 deletions

70
src/debug-panel.js Normal file
View File

@ -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==

131
src/detector.js Normal file
View File

@ -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 (well 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==

35
src/fingerprint-strong.js Normal file
View File

@ -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}`;
};
})();

View File

@ -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==

139
src/paste-submit.js Normal file
View File

@ -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==

43
src/queue.js Normal file
View File

@ -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==

71
src/response-buffer.js Normal file
View File

@ -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==

View File

@ -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);

View File

@ -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==