Update src/ai-repo-commander.user.js
This commit is contained in:
parent
9ddf1f891f
commit
dd0427b598
|
|
@ -83,6 +83,13 @@
|
|||
QUEUE_MAX_PER_MINUTE: 15,
|
||||
QUEUE_MAX_PER_MESSAGE: 5,
|
||||
QUEUE_WAIT_FOR_COMPOSER_MS: 6000,
|
||||
|
||||
RESPONSE_BUFFER_FLUSH_DELAY_MS: 500, // wait for siblings to finish
|
||||
RESPONSE_BUFFER_SECTION_HEADINGS: true,
|
||||
|
||||
MAX_PASTE_CHARS: 250_000, // hard cap per message
|
||||
SPLIT_LONG_RESPONSES: true, // enable multi-message split
|
||||
|
||||
};
|
||||
|
||||
function loadSavedConfig() {
|
||||
|
|
@ -1110,17 +1117,67 @@
|
|||
|
||||
// ---------------------- UI feedback ----------------------
|
||||
class UIFeedback {
|
||||
static appendStatus(sourceElement, templateType, data) {
|
||||
const statusElement = this.createStatusElement(templateType, data);
|
||||
const existing = sourceElement.querySelector('.ai-repo-commander-status');
|
||||
if (existing) existing.remove();
|
||||
sourceElement.appendChild(statusElement);
|
||||
static ensureBoard(containerEl) {
|
||||
let board = containerEl.querySelector('.ai-rc-status-board');
|
||||
if (!board) {
|
||||
board = document.createElement('div');
|
||||
board.className = 'ai-rc-status-board';
|
||||
board.style.cssText = `
|
||||
margin:10px 0;padding:8px;border:1px solid rgba(255,255,255,0.15);
|
||||
border-radius:6px;background:rgba(255,255,255,0.06);font-family:monospace;
|
||||
`;
|
||||
containerEl.appendChild(board);
|
||||
}
|
||||
return board;
|
||||
}
|
||||
|
||||
static appendStatus(containerEl, templateType, data) {
|
||||
// Back-compat: when no key provided, fall through to single-line behavior
|
||||
if (!data || !data.key) {
|
||||
const statusElement = this.createStatusElement(templateType, data);
|
||||
const existing = containerEl.querySelector('.ai-repo-commander-status');
|
||||
if (existing) existing.remove();
|
||||
statusElement.classList.add('ai-repo-commander-status');
|
||||
containerEl.appendChild(statusElement);
|
||||
return;
|
||||
}
|
||||
// Multi-line board (preferred)
|
||||
const board = this.ensureBoard(containerEl);
|
||||
const entry = this.upsertEntry(board, data.key);
|
||||
entry.textContent = this.renderLine(templateType, data);
|
||||
entry.dataset.state = templateType;
|
||||
entry.style.borderLeft = `4px solid ${this.color(templateType)}`;
|
||||
}
|
||||
|
||||
static upsertEntry(board, key) {
|
||||
let el = board.querySelector(`[data-entry-key="${key}"]`);
|
||||
if (!el) {
|
||||
el = document.createElement('div');
|
||||
el.dataset.entryKey = key;
|
||||
el.style.cssText = `
|
||||
padding:6px 8px;margin:4px 0;border-left:4px solid transparent;
|
||||
background:rgba(0,0,0,0.15);border-radius:4px;
|
||||
white-space:pre-wrap;word-break:break-word;
|
||||
`;
|
||||
board.appendChild(el);
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
static renderLine(templateType, data) {
|
||||
const { action, details, label } = data || {};
|
||||
const state = ({
|
||||
SUCCESS:'Success', ERROR:'Error', VALIDATION_ERROR:'Invalid',
|
||||
EXECUTING:'Processing...', MOCK:'Mock'
|
||||
})[templateType] || templateType;
|
||||
const left = label || action || 'Command';
|
||||
return `${left} — ${state}${details ? `: ${details}` : ''}`;
|
||||
}
|
||||
|
||||
static createStatusElement(templateType, data) {
|
||||
const template = STATUS_TEMPLATES[templateType] || STATUS_TEMPLATES.ERROR;
|
||||
const message = template.replace('{action}', data.action).replace('{details}', data.details);
|
||||
const el = document.createElement('div');
|
||||
el.className = 'ai-repo-commander-status';
|
||||
el.textContent = message;
|
||||
el.style.cssText = `
|
||||
padding: 8px 12px; margin: 10px 0; border-radius: 4px;
|
||||
|
|
@ -1130,6 +1187,7 @@
|
|||
`;
|
||||
return el;
|
||||
}
|
||||
|
||||
static color(t) {
|
||||
const c = { SUCCESS:'#10B981', ERROR:'#EF4444', VALIDATION_ERROR:'#F59E0B', EXECUTING:'#3B82F6', MOCK:'#8B5CF6' };
|
||||
return c[t] || '#6B7280';
|
||||
|
|
@ -1303,7 +1361,14 @@
|
|||
try {
|
||||
if (typeof GM_setClipboard === 'function') {
|
||||
GM_setClipboard(payload, { type: 'text', mimetype: 'text/plain' });
|
||||
GM_notification({ title: 'AI Repo Commander', text: 'Content copied to clipboard — press Ctrl/Cmd+V to paste.', timeout: 5000 });
|
||||
RC_DEBUG?.warn('📋 Clipboard fallback used — manual paste may be required', {
|
||||
length: payload.length
|
||||
});
|
||||
GM_notification({
|
||||
title: 'AI Repo Commander',
|
||||
text: 'Content copied to clipboard — press Ctrl/Cmd+V to paste.',
|
||||
timeout: 5000
|
||||
});
|
||||
RC_DEBUG?.info('✅ Paste method succeeded: GM_setClipboard (manual paste required)');
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -1457,24 +1522,35 @@
|
|||
|
||||
// ---------------------- Execution ----------------------
|
||||
class ExecutionManager {
|
||||
static async executeCommand(command, sourceElement) {
|
||||
static async executeCommand(command, sourceElement, renderKey = '', label = '') {
|
||||
try {
|
||||
if ((command.action === 'update_file' || command.action === 'create_file') && !command.commit_message) {
|
||||
command.commit_message = `AI Repo Commander: ${command.path} (${new Date().toISOString()})`;
|
||||
}
|
||||
|
||||
if (!CONFIG.ENABLE_API) return this.mockExecution(command, sourceElement);
|
||||
if (!CONFIG.ENABLE_API) {
|
||||
UIFeedback.appendStatus(sourceElement, 'EXECUTING', { action: command.action, details: 'Mocking...', key: renderKey, label });
|
||||
const res = await this.mockExecution(command, sourceElement, renderKey, label);
|
||||
return res;
|
||||
}
|
||||
|
||||
UIFeedback.appendStatus(sourceElement, 'EXECUTING', { action: command.action, details: 'Making API request...' });
|
||||
UIFeedback.appendStatus(sourceElement, 'EXECUTING', { action: command.action, details: 'Making API request...', key: renderKey, label });
|
||||
|
||||
const res = await this.makeAPICallWithRetry(command);
|
||||
return this.handleSuccess(res, command, sourceElement);
|
||||
return this.handleSuccess(res, command, sourceElement, false, renderKey, label);
|
||||
|
||||
} catch (error) {
|
||||
return this.handleError(error, command, sourceElement);
|
||||
return this.handleError(error, command, sourceElement, renderKey, label);
|
||||
}
|
||||
}
|
||||
|
||||
static async mockExecution(command, sourceElement, renderKey = '', label = '') {
|
||||
await this.delay(500);
|
||||
const mock = { status: 200, responseText: JSON.stringify({ success: true, message: `Mock execution completed for ${command.action}` }) };
|
||||
return this.handleSuccess(mock, command, sourceElement, true, renderKey, label);
|
||||
}
|
||||
|
||||
|
||||
static async makeAPICallWithRetry(command, attempt = 0) {
|
||||
try {
|
||||
requireBridgeKeyIfNeeded();
|
||||
|
|
@ -1519,18 +1595,6 @@
|
|||
});
|
||||
}
|
||||
|
||||
static async mockExecution(command, sourceElement) {
|
||||
await this.delay(500);
|
||||
const mock = {
|
||||
status: 200,
|
||||
responseText: JSON.stringify({
|
||||
success: true,
|
||||
message: `Mock execution completed for ${command.action}`,
|
||||
data: { command: command.action, repo: command.repo, path: command.path, commit_message: command.commit_message }
|
||||
})
|
||||
};
|
||||
return this.handleSuccess(mock, command, sourceElement, true);
|
||||
}
|
||||
|
||||
static _extractGetFileBody(payload) {
|
||||
const item = Array.isArray(payload) ? payload[0] : payload;
|
||||
|
|
@ -1571,20 +1635,21 @@
|
|||
return '```text\n' + lines.join('\n') + '\n```';
|
||||
}
|
||||
|
||||
static async handleSuccess(response, command, sourceElement, isMock = false) {
|
||||
let data;
|
||||
try { data = JSON.parse(response.responseText || '{}'); }
|
||||
static async handleSuccess(response, command, sourceElement, isMock = false, renderKey = '', label = '') {
|
||||
let data; try { data = JSON.parse(response.responseText || '{}'); }
|
||||
catch { data = { message: 'Operation completed (no JSON body)' }; }
|
||||
|
||||
UIFeedback.appendStatus(sourceElement, isMock ? 'MOCK' : 'SUCCESS', {
|
||||
action: command.action,
|
||||
details: data.message || 'Operation completed successfully'
|
||||
details: data.message || 'Operation completed successfully',
|
||||
key: renderKey,
|
||||
label
|
||||
});
|
||||
|
||||
if (command.action === 'get_file') {
|
||||
const body = this._extractGetFileBody(data);
|
||||
if (typeof body === 'string' && body.length) {
|
||||
await pasteAndMaybeSubmit(body);
|
||||
RESP_BUFFER.push({ label, content: body });
|
||||
} else {
|
||||
GM_notification({ title: 'AI Repo Commander', text: 'get_file succeeded, but no content to paste.', timeout: 4000 });
|
||||
}
|
||||
|
|
@ -1594,21 +1659,27 @@
|
|||
const files = this._extractFilesArray(data);
|
||||
if (files && files.length) {
|
||||
const listing = this._formatFilesListing(files);
|
||||
await pasteAndMaybeSubmit(listing);
|
||||
RESP_BUFFER.push({ label, content: listing });
|
||||
} else {
|
||||
const fallback = '```json\n' + JSON.stringify(data, null, 2) + '\n```';
|
||||
await pasteAndMaybeSubmit(fallback);
|
||||
GM_notification({ title: 'AI Repo Commander', text: 'list_files succeeded, but response had no obvious files array. Pasted raw JSON.', timeout: 5000 });
|
||||
RESP_BUFFER.push({ label, content: fallback });
|
||||
GM_notification({
|
||||
title: 'AI Repo Commander',
|
||||
text: 'list_files succeeded, but response had no obvious files array. Pasted raw JSON.',
|
||||
timeout: 5000
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, data, isMock };
|
||||
}
|
||||
|
||||
static handleError(error, command, sourceElement) {
|
||||
static handleError(error, command, sourceElement, renderKey = '', label = '') {
|
||||
UIFeedback.appendStatus(sourceElement, 'ERROR', {
|
||||
action: command.action || 'Command',
|
||||
details: error.message
|
||||
details: error.message,
|
||||
key: renderKey,
|
||||
label
|
||||
});
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
|
@ -1670,6 +1741,114 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const RESP_BUFFER = new ResponseBuffer();
|
||||
window.AI_REPO_RESPONSES = RESP_BUFFER; // optional debug handle
|
||||
|
||||
// ---------------------- Bridge Key ----------------------
|
||||
let BRIDGE_KEY = null;
|
||||
|
||||
|
|
@ -1979,37 +2158,32 @@
|
|||
const messageId = this.getReadableMessageId(element);
|
||||
const subId = `${messageId}#${idx + 1}`;
|
||||
|
||||
// track this sub-command so updateState/attachRetryUI can work
|
||||
this.trackedMessages.set(subId, {
|
||||
element,
|
||||
originalText: hit.text,
|
||||
state: COMMAND_STATES.DETECTED,
|
||||
startTime: Date.now(),
|
||||
lastUpdate: Date.now(),
|
||||
cancelToken: { cancelled: false },
|
||||
});
|
||||
|
||||
execQueue.push(async () => {
|
||||
// optional tiny settle for streaming
|
||||
await ExecutionManager.delay(CONFIG.SETTLE_POLL_MS);
|
||||
|
||||
const allNow = findAllCommandsInMessage(element);
|
||||
const liveForIdx = allNow[idx]?.text;
|
||||
const finalTxt = (liveForIdx && this.isCompleteCommandText(liveForIdx)) ? liveForIdx : hit.text;
|
||||
|
||||
const finalTxt = hit.text; // <<< ADD THIS
|
||||
let parsed;
|
||||
try {
|
||||
parsed = CommandParser.parseYAMLCommand(finalTxt);
|
||||
const val = CommandParser.validateStructure(parsed);
|
||||
if (!val.isValid) throw new Error(`Validation failed: ${val.errors.join(', ')}`);
|
||||
} catch (err) {
|
||||
UIFeedback.appendStatus(element, 'ERROR', { action: 'Command', details: err.message });
|
||||
UIFeedback.appendStatus(element, 'ERROR', {
|
||||
action: 'Command',
|
||||
details: err.message,
|
||||
key: subId, // <<< key per sub-command
|
||||
label: `[${idx+1}] parse`
|
||||
});
|
||||
this.attachRetryUI(element, subId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateState(subId, COMMAND_STATES.EXECUTING);
|
||||
const res = await ExecutionManager.executeCommand(parsed, element);
|
||||
const res = await ExecutionManager.executeCommand(
|
||||
parsed,
|
||||
element,
|
||||
/* renderKey: */ subId, // <<< pass key down
|
||||
/* label: */ `[${idx+1}] ${this.extractAction(finalTxt)}`
|
||||
);
|
||||
|
||||
if (!res || res.success === false) {
|
||||
this.updateState(subId, COMMAND_STATES.ERROR);
|
||||
this.attachRetryUI(element, subId);
|
||||
|
|
@ -2018,6 +2192,7 @@
|
|||
this.updateState(subId, COMMAND_STATES.COMPLETE);
|
||||
});
|
||||
}
|
||||
|
||||
isAssistantMessage(el) {
|
||||
if (!CONFIG.ASSISTANT_ONLY) return true;
|
||||
const host = location.hostname;
|
||||
|
|
@ -2214,7 +2389,11 @@
|
|||
|
||||
// 4) Execute
|
||||
this.updateState(messageId, COMMAND_STATES.EXECUTING);
|
||||
const result = await ExecutionManager.executeCommand(parsed, message.element);
|
||||
|
||||
const action = parsed?.action || 'unknown';
|
||||
const renderKey = `${messageId}#1`;
|
||||
const label = `[1] ${action}`;
|
||||
const result = await ExecutionManager.executeCommand(parsed, message.element, renderKey, label);
|
||||
|
||||
if (!result || result.success === false) {
|
||||
RC_DEBUG?.warn('Execution reported failure', { messageId });
|
||||
|
|
|
|||
Loading…
Reference in New Issue