Compare commits

..

5 Commits

12 changed files with 462 additions and 130 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
# IDE project files
.idea/
*.iml
# OS-specific junk (harmless to keep ignored)
.DS_Store
Thumbs.db

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,7 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>testid</w>
</words>
</dictionary>
</component>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/AI-Repo-Commander.iml" filepath="$PROJECT_DIR$/.idea/AI-Repo-Commander.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -4,18 +4,79 @@
A browser userscript that enables AI assistants to securely interact with git repositories via YAML-style commands, with comprehensive safety measures and real-time feedback.
## 1.1 Diagrams (PlantUML)
- Architecture Overview: Docs/diagrams/architecture-overview.puml
- Command Execution Sequence: Docs/diagrams/sequence-command-execution.puml
- Command Processing State Machine: Docs/diagrams/state-machine.puml
How to view: open the .puml files with any PlantUML viewer (IDE plugin or web renderer) to generate images.
## 2. Core Architecture
### 2.1 Safety-First Design
```javascript
// Configuration - MUST be manually enabled for production
const CONFIG = {
ENABLE_API: false, // Master kill switch
DEBUG_MODE: true, // Development logging
DEBOUNCE_DELAY: 5000, // 5-second bot typing protection
MAX_RETRIES: 2, // API retry attempts
VERSION: '1.0.0'
// Configuration (persisted via localStorage)
const DEFAULT_CONFIG = {
ENABLE_API: true,
DEBUG_MODE: true,
DEBUG_LEVEL: 2,
DEBUG_WATCH_MS: 120000,
DEBUG_MAX_LINES: 400,
DEBUG_SHOW_PANEL: true,
// Timing & API
DEBOUNCE_DELAY: 6500, // streaming debounce before settle
MAX_RETRIES: 2,
VERSION: '1.6.2',
API_TIMEOUT_MS: 60000,
PROCESS_EXISTING: false, // resume-safe guard
ASSISTANT_ONLY: true,
BRIDGE_KEY: '',
// Persistent dedupe window
DEDUPE_TTL_MS: 30 * 24 * 60 * 60 * 1000, // 30 days
COLD_START_MS: 2000, // avoid immediate re-exec on reload
SHOW_EXECUTED_MARKER: true,
// Housekeeping
CLEANUP_AFTER_MS: 30000,
CLEANUP_INTERVAL_MS: 60000,
// Paste + submit behavior
APPEND_TRAILING_NEWLINE: true,
AUTO_SUBMIT: true,
POST_PASTE_DELAY_MS: 250,
SUBMIT_MODE: 'button_first',
// Streaming complete hardening
REQUIRE_TERMINATOR: true, // requires @end@
SETTLE_CHECK_MS: 1300, // stable window after last change
SETTLE_POLL_MS: 250, // settle poll frequency
// Runtime toggles
RUNTIME: { PAUSED: false },
// Hardening & perf
STUCK_AFTER_MS: 10 * 60 * 1000,
SCAN_DEBOUNCE_MS: 400,
FAST_WARN_MS: 50,
SLOW_WARN_MS: 60_000,
// Queue management
QUEUE_MIN_DELAY_MS: 800,
QUEUE_MAX_PER_MINUTE: 15,
QUEUE_MAX_PER_MESSAGE: 5,
QUEUE_WAIT_FOR_COMPOSER_MS: 6000,
RESPONSE_BUFFER_FLUSH_DELAY_MS: 500,
RESPONSE_BUFFER_SECTION_HEADINGS: true,
MAX_PASTE_CHARS: 250_000,
SPLIT_LONG_RESPONSES: true,
};
```
@ -44,7 +105,7 @@ class CommandMonitor {
// Message states
static STATES = {
DETECTED: 'detected', // ^%$bridge found
DETECTED: 'detected', // @bridge@ found
PARSING: 'parsing', // YAML parsing in progress
VALIDATING: 'validating', // Field validation
DEBOUNCING: 'debouncing', // 5-second wait period
@ -53,9 +114,9 @@ class CommandMonitor {
ERROR: 'error' // Command failed
};
scanMessages(): void // Find new command messages
trackMessage(element, text): void // Begin processing pipeline
updateState(messageId, state): void // State machine transitions
scanMessages(): void; // Find new command messages
trackMessage(element, text): void; // Begin processing pipeline
updateState(messageId, state): void; // State machine transitions
}
```
@ -66,14 +127,14 @@ class CommandMonitor {
**Input Format:**
```yaml
^%$bridge
@bridge@
action: update_file
repo: ai-workflow-test
path: README.md
content: |
Multi-line content
with proper formatting
---
@end@
```
**Parsing Logic:**
@ -81,7 +142,7 @@ content: |
```javascript
class CommandParser {
parseYAMLCommand(text): ParsedCommand {
// 1. Extract command block (^%$bridge to ---)
// 1. Extract command block (@bridge@ ... @end@ terminator required)
// 2. Parse key-value pairs
// 3. Handle multi-line content with | syntax
// 4. Set defaults (url, owner if missing)
@ -196,19 +257,78 @@ class UIFeedback {
}
```
### 3.6 Execution Queue
Purpose: Serialize command execution with rate limits and minimal spacing between actions.
Highlights:
- Min delay between tasks: CONFIG.QUEUE_MIN_DELAY_MS
- Rate cap: CONFIG.QUEUE_MAX_PER_MINUTE
- Fire-and-forget push; internal drain loop ensures sequential execution
```javascript
class ExecutionQueue {
push(task) { /* queue and begin drain */ }
async _drain() { /* rate-limit and run */ }
}
```
### 3.7 Response Buffer (paste pipeline)
Purpose: Collect response chunks (e.g., get_file content or list_files listing) and paste them into the composer once nearby tasks finish, respecting size limits.
Highlights:
- Delayed flush (CONFIG.RESPONSE_BUFFER_FLUSH_DELAY_MS) to batch sibling results
- Splitting long responses (CONFIG.SPLIT_LONG_RESPONSES, CONFIG.MAX_PASTE_CHARS)
- Uses pasteAndMaybeSubmit(text) which pastes and optionally auto-submits
### 3.8 Conversation-Aware History (dedupe)
Purpose: Avoid re-executing the same command in the same conversation across reloads.
Highlights:
- Fingerprint of message element (plus optional sub-index) stored in localStorage with TTL (CONFIG.DEDUPE_TTL_MS)
- Per-message, per-command "Run again" UI allows manual re-exec
- SHOW_EXECUTED_MARKER outlines executed messages in green
### 3.9 Paste + Submit Mechanics
Purpose: Robustly paste text into the site-specific composer and submit.
Paste flow (tries in order):
- Dispatch ClipboardEvent('paste', clipboardData)
- ProseMirror innerHTML paragraphs with input/change events
- Selection/Range insertion for contentEditable; setRangeText for inputs/textareas
- Direct value/textContent assignment as a fallback
- GM_setClipboard as last resort (manual paste)
Submit flow:
- Find send button scoped to nearest form/composer/main first
- If no button or enter-only mode, simulate Enter key events on the input
### 3.10 Notifications
Purpose: Present OS-level notifications via userscript APIs when available.
- Implementation: Direct calls to GM_notification for OS-level notifications.
- Userscript header includes: `@grant GM_notification` to enable the API in Tampermonkey/Violentmonkey.
- IDE hint: A top-of-file comment `/* global GM_notification */` is present to silence unresolved symbol warnings in WebStorm.
- Portability note: No wrapper is used. If the script is executed outside a userscript manager, GM_notification will not exist and notifications are skipped; the in-page debug panel/toasts provide fallback visibility.
## 4. Processing Pipeline
### 4.1 Step-by-Step Flow
```
1. MONITOR: Detect ^%$bridge in new messages
2. TRACK: Assign unique ID and initial state
3. PARSE: Extract YAML into structured command
4. VALIDATE: Check required fields and formats
5. DEBOUNCE: Wait 5 seconds for bot typing completion
6. EXECUTE: Make API call (or mock if disabled)
7. FEEDBACK: Replace with status message
8. CLEANUP: Update state to complete
1. MONITOR: Detect @bridge@ command blocks in assistant messages; extract all complete blocks per message.
2. TRACK: Assign readable message ID; mark per-command history to avoid re-exec; show queue badge.
3. PARSE: Extract inner YAML-like key/values from @bridge@ ... @end@; apply defaults; expand owner/repo shorthand.
4. VALIDATE: Check required fields based on action; treat examples specially (skip execution).
5. DEBOUNCE: Wait DEBOUNCE_DELAY, then run SETTLE window to ensure the final, stable command text.
6. EXECUTE (queue): Serialize via ExecutionQueue with rate limits; GM_xmlhttpRequest to bridge with retries.
7. FEEDBACK: Render per-command status lines; paste response bodies via ResponseBuffer into the composer.
8. SUBMIT: Optionally auto-submit after paste using scoped send button or Enter key events.
9. COMPLETE: Update state and timestamps; maintain history TTL; attach per-command "Run again" buttons for manual control.
```
### 4.2 Error Handling Flow
@ -224,12 +344,14 @@ Network Error → [Action: Error] Cannot reach bridge
### 5.1 Multiple Protection Layers
- **API Master Switch:** ENABLE_API must be explicitly true
- **Command Validation:** Required fields and format checking
- **Bot Debouncing:** 5-second wait prevents partial command execution
- **Network Timeouts:** 30-second API call limits
- **Error Boundaries:** Isolated error handling per command
- **State Tracking:** Prevents duplicate processing
- API Master Switch: ENABLE_API gate and runtime PAUSED toggle
- Command Validation: required fields per action, example detection
- Streaming Hardened: DEBOUNCE_DELAY + SETTLE_CHECK_MS window ensures complete blocks
- Rate Limiting: ExecutionQueue caps per-minute and enforces inter-task delay
- Persistent Dedupe: conversation-aware history with TTL to prevent re-exec across reloads
- UI Safeguards: per-command "Run again" instead of auto-reexec; executed marker on messages
- Network Limits: API_TIMEOUT_MS and bounded retries
- Error Isolation: per-command try/catch with inline validation handling (no throw/catch ping-pong)
### 5.2 Emergency Stop Options
@ -251,18 +373,10 @@ window.AI_REPO_STOP = emergencyStop;
```javascript
const PLATFORM_SELECTORS = {
'chat.openai.com': {
messages: '[class*="message"]',
input: '#prompt-textarea'
},
'claude.ai': {
messages: '.chat-message',
input: '[contenteditable="true"]'
},
'gemini.google.com': {
messages: '.message-content',
input: 'textarea, [contenteditable="true"]'
}
'chat.openai.com': { messages: '[data-message-author-role]', input: '#prompt-textarea, textarea, [contenteditable="true"]', content: '.markdown' },
'chatgpt.com': { messages: '[data-message-author-role]', input: '#prompt-textarea, textarea, [contenteditable="true"]', content: '.markdown' },
'claude.ai': { messages: '.chat-message', input: '[contenteditable="true"]', content: '.content' },
'gemini.google.com': { messages: '.message-content', input: 'textarea, [contenteditable="true"]', content: '.message-text' }
};
```
@ -273,25 +387,9 @@ const PLATFORM_SELECTORS = {
```javascript
// Test command generator for safe testing
const TEST_COMMANDS = {
validUpdate: `^%$bridge
action: update_file
repo: test-repo
path: TEST.md
content: |
Test content
Multiple lines
---`,
invalidCommand: `^%$bridge
action: update_file
repo: test-repo
---`,
getFile: `^%$bridge
action: get_file
repo: test-repo
path: README.md
---`
validUpdate: '@bridge@\naction: update_file\nrepo: test-repo\npath: TEST.md\ncontent: |\n Test content\n Multiple lines\n@end@',
invalidCommand: '@bridge@\naction: update_file\nrepo: test-repo\n@end@',
getFile: '@bridge@\naction: get_file\nrepo: test-repo\npath: README.md\n@end@'
};
```

View File

@ -0,0 +1,45 @@
@startuml Architecture Overview
skinparam componentStyle rectangle
skinparam shadowing false
skinparam ArrowColor #555
skinparam component {
BackgroundColor<<core>> #E8F5E9
BackgroundColor<<ui>> #E3F2FD
BackgroundColor<<ext>> #FFF3E0
}
package "Browser (Chat Sites)" <<ext>> {
[ChatGPT DOM]
[Claude DOM]
[Gemini DOM]
}
node "Userscript: AI Repo Commander" as Userscript {
component "Core Monitor" <<core>> as Monitor
component "Command Parser" <<core>> as Parser
component "Validation Engine" <<core>> as Validator
component "Execution Manager" <<core>> as Executor
component "Dedupe Store" <<core>> as Dedupe
component "Config Manager" <<core>> as Config
component "UI Panel (Tools/Settings)" <<ui>> as Panel
component "Inline Status UI" <<ui>> as InlineUI
}
cloud "Bridge API" <<ext>> as Bridge
' Relationships
[ChatGPT DOM] -down-> Monitor : observe assistant messages
[Claude DOM] -down-> Monitor
[Gemini DOM] -down-> Monitor
Monitor -> Parser : extract YAML blocks
Parser -> Validator : validate fields/actions
Validator -> Executor : approved commands
Executor -> Bridge : HTTP requests (key, action)
Executor --> InlineUI : progress + results
Monitor --> InlineUI : markers (processed, run again)
Panel <--> Config : view/edit config
Executor <--> Config : timeouts, retries
Monitor <--> Dedupe : per-convo records
@enduml

View File

@ -0,0 +1,37 @@
@startuml Command Execution Sequence
skinparam shadowing false
skinparam ArrowColor #555
actor User as U
participant "Assistant Message\n(DOM)" as DOM
participant "Core Monitor" as Monitor
participant "Command Parser" as Parser
participant "Validation Engine" as Validator
participant "Dedupe Store" as Dedupe
participant "Execution Manager" as Executor
participant "Inline Status UI" as UI
participant "Bridge API" as Bridge
U -> DOM : Produces message with YAML block
DOM -> Monitor : observe new/updated message
Monitor -> Monitor : debounce + streaming settle
Monitor -> Parser : extract YAML block
Parser --> Monitor : ParsedCommand
Monitor -> Validator : validate fields and action
Validator --> Monitor : ValidationResult
Monitor -> Dedupe : check de-duplication
Dedupe --> Monitor : isNew?
alt example: true or duplicate
Monitor -> UI : mark as processed (skipped)
return
else valid and new
Monitor -> UI : show "Ready" (runnable)
Monitor -> Executor : enqueue/execute command
Executor -> UI : state = executing
Executor -> Bridge : POST action with bridge key
Bridge --> Executor : response (success/failure)
Executor -> UI : show result + Run Again button
Executor -> Dedupe : record execution (ttl 30d)
end
@enduml

View File

@ -0,0 +1,42 @@
@startuml Command Processing State Machine
skinparam shadowing false
skinparam ArrowColor #555
skinparam state {
StartColor #A5D6A7
BackgroundColor #FAFAFA
}
[*] --> DETECTED : YAML block found
state DETECTED {
}
DETECTED --> PARSING : debounce/settle passed
PARSING --> VALIDATING : YAML parsed
PARSING --> ERROR : parse failure
VALIDATING --> DEDUPE_CHECK : required fields ok
VALIDATING --> ERROR : validation failed
state DEDUPE_CHECK
DEDUPE_CHECK --> SKIPPED : duplicate or example:true
DEDUPE_CHECK --> READY : new and runnable
state READY
READY --> EXECUTING : user intent or auto-exec policy
READY --> [*] : STOP triggered
state EXECUTING
EXECUTING --> COMPLETE : success
EXECUTING --> ERROR : API/network failure
state SKIPPED
SKIPPED --> [*]
state COMPLETE
COMPLETE --> [*]
state ERROR
ERROR --> [*]
@enduml

View File

@ -74,6 +74,17 @@ SLOW_WARN_MS: 60000
5) Show inline status and store dedupe record
6) Expose a **Run Again** button for intentional re-execution
## Diagrams (PlantUML)
The following PlantUML diagrams provide a high-level overview of the codebase:
- Architecture Overview: Docs/diagrams/architecture-overview.puml
- Command Execution Sequence: Docs/diagrams/sequence-command-execution.puml
- Command Processing State Machine: Docs/diagrams/state-machine.puml
How to view:
- Use any PlantUML renderer (IntelliJ/VSCode plugin, plantuml.com/plantuml, or local PlantUML jar)
- Copy the contents of a .puml file into your renderer to generate the diagram image
## New in v1.6.2
- **Resume-Safe Guard:** On resume, treat like a cold start and mark visible commands as processed, preventing accidental re-execution.
- **Example Field Support:** Add `example: true` to any command block to make it inert. Its silently skipped (no error UI).

View File

@ -2,7 +2,7 @@
// @name AI Repo Commander
// @namespace http://tampermonkey.net/
// @version 1.6.2
// @description Execute @bridge@ YAML commands from AI assistants (safe & robust): complete-block detection, streaming-settle, persistent dedupe, paste+autosubmit, debug console with Tools/Settings, draggable/collapsible panel, multi-command queue
// @description Execute @bridge@ YAML commands from AI assistants (safe & robust): complete-block detection, streaming-settle, persistent dedupe, paste + auto-submit, debug console with Tools/Settings, draggable/collapsible panel, multi-command queue
// @author Your Name
// @match https://chat.openai.com/*
// @match https://chatgpt.com/*
@ -14,6 +14,9 @@
// @connect n8n.brrd.tech
// @connect *
// ==/UserScript==
/* global GM_notification */
/* global GM_setClipboard */
/* global GM_xmlhttpRequest */
(function () {
'use strict';
@ -98,8 +101,7 @@
const raw = localStorage.getItem(STORAGE_KEYS.cfg);
if (!raw) return structuredClone(DEFAULT_CONFIG);
const saved = JSON.parse(raw);
const merged = { ...DEFAULT_CONFIG, ...saved, RUNTIME: { ...DEFAULT_CONFIG.RUNTIME, ...(saved.RUNTIME || {}) } };
return merged;
return { ...DEFAULT_CONFIG, ...saved, RUNTIME: { ...DEFAULT_CONFIG.RUNTIME, ...(saved.RUNTIME || {}) } };
} catch {
return structuredClone(DEFAULT_CONFIG);
}
@ -192,17 +194,35 @@
_fallbackCopy(text, originalError = null) {
try {
// Show a minimal manual copy UI (no deprecated execCommand)
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;inset:0;z-index:999999;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;';
const panel = document.createElement('div');
panel.style.cssText = 'background:#1f2937;color:#e5e7eb;padding:12px 12px 8px;border-radius:8px;width:min(760px,90vw);max-height:70vh;display:flex;flex-direction:column;gap:8px;box-shadow:0 10px 30px rgba(0,0,0,0.5)';
const title = document.createElement('div');
title.textContent = 'Copy to clipboard';
title.style.cssText = 'font:600 14px system-ui,sans-serif;';
const hint = document.createElement('div');
hint.textContent = 'Press Ctrl+C (Windows/Linux) or ⌘+C (macOS) to copy the selected text.';
hint.style.cssText = 'font:12px system-ui,sans-serif;opacity:0.85;';
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.readOnly = true;
ta.style.cssText = 'width:100%;height:40vh;font:12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;';
const row = document.createElement('div');
row.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;';
const close = document.createElement('button');
close.textContent = 'Close';
close.style.cssText = 'padding:6px 10px;background:#374151;color:#e5e7eb;border:1px solid #4b5563;border-radius:6px;cursor:pointer;';
close.onclick = () => overlay.remove();
panel.append(title, hint, ta, row);
row.append(close);
overlay.append(panel);
document.body.appendChild(overlay);
// Focus and select
ta.focus();
ta.select();
const ok = document.execCommand('copy');
document.body.removeChild(ta);
if (ok) this.info(`Copied last ${text.split('\n').length} lines to clipboard (fallback)`);
else this.warn('Clipboard copy failed (fallback)');
this.warn('Clipboard API unavailable; showing manual copy UI', { error: originalError?.message });
} catch (e) {
this.warn('Clipboard copy failed', { error: originalError?.message || e.message });
}
@ -691,7 +711,7 @@
}
// Extract the *command block* if present; else fall back to element text
function _commandishText(el) {
function _commandLikeText(el) {
// Mirror parser's detector: require header, action, and '@end@'
const blocks = el.querySelectorAll('pre code, pre, code');
for (const b of blocks) {
@ -706,7 +726,7 @@
// Hash of the command (or element text) capped to 2000 chars
function _hashCommand(el) {
const t = _commandishText(el);
const t = _commandLikeText(el);
return _hash(t.slice(0, 2000));
}
@ -914,8 +934,9 @@
'create_release': ['action', 'repo', 'tag_name', 'name']
};
const FIELD_VALIDATORS = {
repo: (v) => /^[\w\-\.]+(\/[\w\-\.]+)?$/.test(v),
// noinspection JSUnusedGlobalSymbols
const FIELD_VALIDATORS = {
repo: (v) => /^[\w\-.]+(\/[\w\-.]+)?$/.test(v),
path: (v) => !v.includes('..') && !v.startsWith('/') && !v.includes('\\'),
action: (v) => Object.keys(REQUIRED_FIELDS).includes(v),
url: (v) => !v || /^https?:\/\/[^/\s]+(?:\/|$)/i.test(v),
@ -1001,9 +1022,13 @@
}
}
/**
* @param {Element} el
* @param {string|number} [suffix]
*/
hasElement(el, suffix = '') {
let fp = fingerprintElement(el);
if (suffix) fp += `#${suffix}`;
if (suffix !== '' && suffix != null) fp += `#${String(suffix)}`;
const result = this.session.has(fp) || (fp in this.cache);
if (result && CONFIG.DEBUG_LEVEL >= 4) {
@ -1017,9 +1042,13 @@
return result;
}
/**
* @param {Element} el
* @param {string|number} [suffix]
*/
markElement(el, suffix = '') {
let fp = fingerprintElement(el);
if (suffix) fp += `#${suffix}`;
if (suffix !== '' && suffix != null) fp += `#${String(suffix)}`;
this.session.add(fp);
this.cache[fp] = Date.now();
this._save();
@ -1028,7 +1057,7 @@
fingerprint: fp.slice(0, 60) + '...'
});
if (CONFIG.SHOW_EXECUTED_MARKER) {
if (CONFIG.SHOW_EXECUTED_MARKER && el instanceof HTMLElement) {
try {
el.style.borderLeft = '3px solid #10B981';
el.title = 'Command executed — use "Run again" to re-run';
@ -1036,20 +1065,6 @@
}
}
unmarkElement(el, suffix = '') {
let fp = fingerprintElement(el);
if (suffix) fp += `#${suffix}`;
this.session.delete(fp);
if (fp in this.cache) {
delete this.cache[fp];
this._save();
}
RC_DEBUG?.verbose('Unmarked element', {
fingerprint: fp.slice(0, 60) + '...'
});
}
resetAll() {
this.session.clear();
localStorage.removeItem(this.key);
@ -1059,7 +1074,8 @@
}
// Global helpers (stable)
window.AI_REPO = {
// noinspection JSUnusedGlobalSymbols
window.AI_REPO = {
clearHistory: () => {
try { commandMonitor?.history?.resetAll?.(); } catch {}
localStorage.removeItem(STORAGE_KEYS.history); // legacy
@ -1289,7 +1305,7 @@
'form button[type="submit"]'
];
for (const s of selectors) {
const btn = document.querySelector(s);
const btn = scope.querySelector(s);
if (!btn) continue;
const style = window.getComputedStyle(btn);
const disabled = btn.disabled || btn.getAttribute('aria-disabled') === 'true';
@ -1369,37 +1385,52 @@
// Pad with blank lines before/after to preserve ``` fences visually.
const payload2 = `\n${payload.replace(/\n?$/, '\n')}\n`;
const escape = (s) => s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
const html = String(payload2)
.split('\n')
.map(line => line.length ? `<p>${escape(line)}</p>` : '<p><br></p>')
.join('');
el.innerHTML = html;
el.innerHTML = String(payload2)
.split('\n')
.map(line => line.length ? `<p>${escape(line)}</p>` : '<p><br></p>')
.join('');
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
RC_DEBUG?.info('✅ Paste method succeeded: ProseMirror');
return true;
}
// Method 3: execCommand
// Method 3: Selection API insertion (non-deprecated)
try {
const sel = window.getSelection && window.getSelection();
if (sel && sel.rangeCount === 0 && el instanceof HTMLElement) {
const r = document.createRange();
r.selectNodeContents(el); r.collapse(false); sel.removeAllRanges(); sel.addRange(r);
RC_DEBUG?.verbose('Selection range set for execCommand');
}
const success = document.execCommand && document.execCommand('insertText', false, payload);
RC_DEBUG?.verbose('execCommand attempt', { success });
if (success) {
RC_DEBUG?.info('✅ Paste method succeeded: execCommand');
if (el.isContentEditable || el.getAttribute('contenteditable') === 'true') {
// Ensure there's a caret; if not, place it at the end
if (sel && sel.rangeCount === 0) {
const r = document.createRange();
r.selectNodeContents(el); r.collapse(false); sel.removeAllRanges(); sel.addRange(r);
RC_DEBUG?.verbose('Selection range set for contentEditable');
}
if (sel && sel.rangeCount > 0) {
const range = sel.getRangeAt(0);
range.deleteContents();
const node = document.createTextNode(payload);
range.insertNode(node);
// Move caret after inserted node
range.setStartAfter(node);
range.setEndAfter(node);
sel.removeAllRanges(); sel.addRange(range);
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
RC_DEBUG?.info('✅ Paste method succeeded: Selection API (contentEditable)');
return true;
}
} else if (typeof el.setRangeText === 'function') {
// For inputs/text-areas supporting setRangeText
const start = el.selectionStart ?? 0;
const end = el.selectionEnd ?? start;
el.setRangeText(payload, start, end, 'end');
el.dispatchEvent(new Event('input', { bubbles: true }));
RC_DEBUG?.info('✅ Paste method succeeded: setRangeText');
return true;
}
} catch (e) {
RC_DEBUG?.verbose('execCommand failed', { error: String(e) });
RC_DEBUG?.verbose('Selection API insertion failed', { error: String(e) });
}
// Method 4: TEXTAREA/INPUT
@ -1594,8 +1625,7 @@
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;
return await this.mockExecution(command, sourceElement, renderKey, label);
}
UIFeedback.appendStatus(sourceElement, 'EXECUTING', { action: command.action, details: 'Making API request...', key: renderKey, label });
@ -1713,7 +1743,7 @@
if (command.action === 'get_file') {
const body = this._extractGetFileBody(data);
if (typeof body === 'string' && body.length) {
RESP_BUFFER.push({ label, content: body });
new ResponseBuffer().push({ label, content: body });
} else {
GM_notification({ title: 'AI Repo Commander', text: 'get_file succeeded, but no content to paste.', timeout: 4000 });
}
@ -1723,10 +1753,10 @@
const files = this._extractFilesArray(data);
if (files && files.length) {
const listing = this._formatFilesListing(files);
RESP_BUFFER.push({ label, content: listing });
new ResponseBuffer().push({ label, content: listing });
} else {
const fallback = '```json\n' + JSON.stringify(data, null, 2) + '\n```';
RESP_BUFFER.push({ label, content: fallback });
new ResponseBuffer().push({ label, content: fallback });
GM_notification({
title: 'AI Repo Commander',
text: 'list_files succeeded, but response had no obvious files array. Pasted raw JSON.',
@ -1764,7 +1794,7 @@
push(task) {
this.q.push(task);
this.onSizeChange?.(this.q.length);
if (!this.running) this._drain();
if (!this.running) void this._drain();
}
clear() {
this.q.length = 0;
@ -1799,7 +1829,8 @@
}
const execQueue = new ExecutionQueue();
window.AI_REPO_QUEUE = {
// noinspection JSUnusedGlobalSymbols
window.AI_REPO_QUEUE = {
clear: () => execQueue.clear(),
size: () => execQueue.q.length,
cancelOne: (cb) => execQueue.cancelOne(cb),
@ -1910,8 +1941,8 @@
}
const RESP_BUFFER = new ResponseBuffer();
window.AI_REPO_RESPONSES = RESP_BUFFER; // optional debug handle
window.AI_REPO_RESPONSES = new ResponseBuffer(); // optional debug handle
// ---------------------- Bridge Key ----------------------
let BRIDGE_KEY = null;
@ -2229,8 +2260,6 @@
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',
@ -2241,6 +2270,17 @@
this.attachRetryUI(element, subId);
return;
}
const val = CommandParser.validateStructure(parsed);
if (!val.isValid) {
UIFeedback.appendStatus(element, 'ERROR', {
action: 'Command',
details: `Validation failed: ${val.errors.join(', ')}`,
key: subId,
label: `[${idx+1}] parse`
});
this.attachRetryUI(element, subId);
return;
}
this.updateState(subId, COMMAND_STATES.EXECUTING);
const res = await ExecutionManager.executeCommand(
@ -2285,7 +2325,7 @@
cancelToken: { cancelled: false }
});
this.updateState(messageId, COMMAND_STATES.PARSING);
this.processCommand(messageId);
void this.processCommand(messageId);
}
async debounceWithCancel(messageId) {
@ -2392,8 +2432,8 @@
// Highlight failed one
try {
const bar = element.querySelector('.ai-rc-rerun');
const btns = [...bar.querySelectorAll('button')].filter(b => /Run again \[#\d+\]/.test(b.textContent || ''));
const b = btns[failedIdx];
const buttons = [...bar.querySelectorAll('button')].filter(b => /Run again \[#\d+]/.test(b.textContent || ''));
const b = buttons[failedIdx];
if (b) b.style.outline = '2px solid #ef4444';
} catch {}
}
@ -2467,8 +2507,13 @@
}
if (!validation.isValid) {
this.updateState(messageId, COMMAND_STATES.ERROR);
this.attachRetryUI(message.element, messageId);
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
UIFeedback.appendStatus(message.element, 'ERROR', {
action: 'Command',
details: `Validation failed: ${validation.errors.join(', ')}`
});
return;
}
// 3) Debounce
@ -2524,8 +2569,13 @@
const reParsed = CommandParser.parseYAMLCommand(stable);
const reVal = CommandParser.validateStructure(reParsed);
if (!reVal.isValid) {
this.updateState(messageId, COMMAND_STATES.ERROR);
this.attachRetryUI(message.element, messageId);
throw new Error(`Final validation failed: ${reVal.errors.join(', ')}`);
UIFeedback.appendStatus(message.element, 'ERROR', {
action: 'Command',
details: `Final validation failed: ${reVal.errors.join(', ')}`
});
return;
}
parsed = reParsed;
}
@ -2647,7 +2697,7 @@
RC_DEBUG?.info('Retrying message now', { messageId });
commandMonitor.updateState(messageId, COMMAND_STATES.PARSING);
commandMonitor.processCommand(messageId);
void commandMonitor.processCommand(messageId);
} catch (e) {
RC_DEBUG?.error('Failed to retry message', { messageId, error: String(e) });
}
@ -2793,7 +2843,8 @@ path: .
if (!commandMonitor) {
commandMonitor = new CommandMonitor();
window.AI_REPO_COMMANDER = {
// noinspection JSUnusedGlobalSymbols
window.AI_REPO_COMMANDER = {
monitor: commandMonitor,
config: CONFIG,
test: TEST_COMMANDS,