Compare commits
No commits in common. "main" and "changes" have entirely different histories.
|
|
@ -1,7 +0,0 @@
|
||||||
# IDE project files
|
|
||||||
.idea/
|
|
||||||
*.iml
|
|
||||||
|
|
||||||
# OS-specific junk (harmless to keep ignored)
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
<?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>
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
<component name="ProjectDictionaryState">
|
|
||||||
<dictionary name="project">
|
|
||||||
<words>
|
|
||||||
<w>testid</w>
|
|
||||||
</words>
|
|
||||||
</dictionary>
|
|
||||||
</component>
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
<?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>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -4,79 +4,18 @@
|
||||||
|
|
||||||
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.
|
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. Core Architecture
|
||||||
|
|
||||||
### 2.1 Safety-First Design
|
### 2.1 Safety-First Design
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Configuration (persisted via localStorage)
|
// Configuration - MUST be manually enabled for production
|
||||||
const DEFAULT_CONFIG = {
|
const CONFIG = {
|
||||||
ENABLE_API: true,
|
ENABLE_API: false, // Master kill switch
|
||||||
DEBUG_MODE: true,
|
DEBUG_MODE: true, // Development logging
|
||||||
DEBUG_LEVEL: 2,
|
DEBOUNCE_DELAY: 5000, // 5-second bot typing protection
|
||||||
DEBUG_WATCH_MS: 120000,
|
MAX_RETRIES: 2, // API retry attempts
|
||||||
DEBUG_MAX_LINES: 400,
|
VERSION: '1.0.0'
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -105,7 +44,7 @@ class CommandMonitor {
|
||||||
|
|
||||||
// Message states
|
// Message states
|
||||||
static STATES = {
|
static STATES = {
|
||||||
DETECTED: 'detected', // @bridge@ found
|
DETECTED: 'detected', // ^%$bridge found
|
||||||
PARSING: 'parsing', // YAML parsing in progress
|
PARSING: 'parsing', // YAML parsing in progress
|
||||||
VALIDATING: 'validating', // Field validation
|
VALIDATING: 'validating', // Field validation
|
||||||
DEBOUNCING: 'debouncing', // 5-second wait period
|
DEBOUNCING: 'debouncing', // 5-second wait period
|
||||||
|
|
@ -114,9 +53,9 @@ class CommandMonitor {
|
||||||
ERROR: 'error' // Command failed
|
ERROR: 'error' // Command failed
|
||||||
};
|
};
|
||||||
|
|
||||||
scanMessages(): void; // Find new command messages
|
scanMessages(): void // Find new command messages
|
||||||
trackMessage(element, text): void; // Begin processing pipeline
|
trackMessage(element, text): void // Begin processing pipeline
|
||||||
updateState(messageId, state): void; // State machine transitions
|
updateState(messageId, state): void // State machine transitions
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -127,14 +66,14 @@ class CommandMonitor {
|
||||||
**Input Format:**
|
**Input Format:**
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@bridge@
|
^%$bridge
|
||||||
action: update_file
|
action: update_file
|
||||||
repo: ai-workflow-test
|
repo: ai-workflow-test
|
||||||
path: README.md
|
path: README.md
|
||||||
content: |
|
content: |
|
||||||
Multi-line content
|
Multi-line content
|
||||||
with proper formatting
|
with proper formatting
|
||||||
@end@
|
---
|
||||||
```
|
```
|
||||||
|
|
||||||
**Parsing Logic:**
|
**Parsing Logic:**
|
||||||
|
|
@ -142,7 +81,7 @@ content: |
|
||||||
```javascript
|
```javascript
|
||||||
class CommandParser {
|
class CommandParser {
|
||||||
parseYAMLCommand(text): ParsedCommand {
|
parseYAMLCommand(text): ParsedCommand {
|
||||||
// 1. Extract command block (@bridge@ ... @end@ terminator required)
|
// 1. Extract command block (^%$bridge to ---)
|
||||||
// 2. Parse key-value pairs
|
// 2. Parse key-value pairs
|
||||||
// 3. Handle multi-line content with | syntax
|
// 3. Handle multi-line content with | syntax
|
||||||
// 4. Set defaults (url, owner if missing)
|
// 4. Set defaults (url, owner if missing)
|
||||||
|
|
@ -257,78 +196,19 @@ 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. Processing Pipeline
|
||||||
|
|
||||||
### 4.1 Step-by-Step Flow
|
### 4.1 Step-by-Step Flow
|
||||||
|
|
||||||
```
|
```
|
||||||
1. MONITOR: Detect @bridge@ command blocks in assistant messages; extract all complete blocks per message.
|
1. MONITOR: Detect ^%$bridge in new messages
|
||||||
2. TRACK: Assign readable message ID; mark per-command history to avoid re-exec; show queue badge.
|
2. TRACK: Assign unique ID and initial state
|
||||||
3. PARSE: Extract inner YAML-like key/values from @bridge@ ... @end@; apply defaults; expand owner/repo shorthand.
|
3. PARSE: Extract YAML into structured command
|
||||||
4. VALIDATE: Check required fields based on action; treat examples specially (skip execution).
|
4. VALIDATE: Check required fields and formats
|
||||||
5. DEBOUNCE: Wait DEBOUNCE_DELAY, then run SETTLE window to ensure the final, stable command text.
|
5. DEBOUNCE: Wait 5 seconds for bot typing completion
|
||||||
6. EXECUTE (queue): Serialize via ExecutionQueue with rate limits; GM_xmlhttpRequest to bridge with retries.
|
6. EXECUTE: Make API call (or mock if disabled)
|
||||||
7. FEEDBACK: Render per-command status lines; paste response bodies via ResponseBuffer into the composer.
|
7. FEEDBACK: Replace with status message
|
||||||
8. SUBMIT: Optionally auto-submit after paste using scoped send button or Enter key events.
|
8. CLEANUP: Update state to complete
|
||||||
9. COMPLETE: Update state and timestamps; maintain history TTL; attach per-command "Run again" buttons for manual control.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4.2 Error Handling Flow
|
### 4.2 Error Handling Flow
|
||||||
|
|
@ -344,14 +224,12 @@ Network Error → [Action: Error] Cannot reach bridge
|
||||||
|
|
||||||
### 5.1 Multiple Protection Layers
|
### 5.1 Multiple Protection Layers
|
||||||
|
|
||||||
- API Master Switch: ENABLE_API gate and runtime PAUSED toggle
|
- **API Master Switch:** ENABLE_API must be explicitly true
|
||||||
- Command Validation: required fields per action, example detection
|
- **Command Validation:** Required fields and format checking
|
||||||
- Streaming Hardened: DEBOUNCE_DELAY + SETTLE_CHECK_MS window ensures complete blocks
|
- **Bot Debouncing:** 5-second wait prevents partial command execution
|
||||||
- Rate Limiting: ExecutionQueue caps per-minute and enforces inter-task delay
|
- **Network Timeouts:** 30-second API call limits
|
||||||
- Persistent Dedupe: conversation-aware history with TTL to prevent re-exec across reloads
|
- **Error Boundaries:** Isolated error handling per command
|
||||||
- UI Safeguards: per-command "Run again" instead of auto-reexec; executed marker on messages
|
- **State Tracking:** Prevents duplicate processing
|
||||||
- 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
|
### 5.2 Emergency Stop Options
|
||||||
|
|
||||||
|
|
@ -373,10 +251,18 @@ window.AI_REPO_STOP = emergencyStop;
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const PLATFORM_SELECTORS = {
|
const PLATFORM_SELECTORS = {
|
||||||
'chat.openai.com': { messages: '[data-message-author-role]', input: '#prompt-textarea, textarea, [contenteditable="true"]', content: '.markdown' },
|
'chat.openai.com': {
|
||||||
'chatgpt.com': { messages: '[data-message-author-role]', input: '#prompt-textarea, textarea, [contenteditable="true"]', content: '.markdown' },
|
messages: '[class*="message"]',
|
||||||
'claude.ai': { messages: '.chat-message', input: '[contenteditable="true"]', content: '.content' },
|
input: '#prompt-textarea'
|
||||||
'gemini.google.com': { messages: '.message-content', input: 'textarea, [contenteditable="true"]', content: '.message-text' }
|
},
|
||||||
|
'claude.ai': {
|
||||||
|
messages: '.chat-message',
|
||||||
|
input: '[contenteditable="true"]'
|
||||||
|
},
|
||||||
|
'gemini.google.com': {
|
||||||
|
messages: '.message-content',
|
||||||
|
input: 'textarea, [contenteditable="true"]'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -387,9 +273,25 @@ const PLATFORM_SELECTORS = {
|
||||||
```javascript
|
```javascript
|
||||||
// Test command generator for safe testing
|
// Test command generator for safe testing
|
||||||
const TEST_COMMANDS = {
|
const TEST_COMMANDS = {
|
||||||
validUpdate: '@bridge@\naction: update_file\nrepo: test-repo\npath: TEST.md\ncontent: |\n Test content\n Multiple lines\n@end@',
|
validUpdate: `^%$bridge
|
||||||
invalidCommand: '@bridge@\naction: update_file\nrepo: test-repo\n@end@',
|
action: update_file
|
||||||
getFile: '@bridge@\naction: get_file\nrepo: test-repo\npath: README.md\n@end@'
|
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
|
||||||
|
---`
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
@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
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
@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
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
|
|
||||||
@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
|
|
||||||
11
README.md
11
README.md
|
|
@ -74,17 +74,6 @@ SLOW_WARN_MS: 60000
|
||||||
5) Show inline status and store dedupe record
|
5) Show inline status and store dedupe record
|
||||||
6) Expose a **Run Again** button for intentional re-execution
|
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
|
## 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.
|
- **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. It’s silently skipped (no error UI).
|
- **Example Field Support:** Add `example: true` to any command block to make it inert. It’s silently skipped (no error UI).
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// @name AI Repo Commander
|
// @name AI Repo Commander
|
||||||
// @namespace http://tampermonkey.net/
|
// @namespace http://tampermonkey.net/
|
||||||
// @version 1.6.2
|
// @version 1.6.2
|
||||||
// @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
|
// @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
|
||||||
// @author Your Name
|
// @author Your Name
|
||||||
// @match https://chat.openai.com/*
|
// @match https://chat.openai.com/*
|
||||||
// @match https://chatgpt.com/*
|
// @match https://chatgpt.com/*
|
||||||
|
|
@ -14,9 +14,6 @@
|
||||||
// @connect n8n.brrd.tech
|
// @connect n8n.brrd.tech
|
||||||
// @connect *
|
// @connect *
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
/* global GM_notification */
|
|
||||||
/* global GM_setClipboard */
|
|
||||||
/* global GM_xmlhttpRequest */
|
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
@ -101,7 +98,8 @@
|
||||||
const raw = localStorage.getItem(STORAGE_KEYS.cfg);
|
const raw = localStorage.getItem(STORAGE_KEYS.cfg);
|
||||||
if (!raw) return structuredClone(DEFAULT_CONFIG);
|
if (!raw) return structuredClone(DEFAULT_CONFIG);
|
||||||
const saved = JSON.parse(raw);
|
const saved = JSON.parse(raw);
|
||||||
return { ...DEFAULT_CONFIG, ...saved, RUNTIME: { ...DEFAULT_CONFIG.RUNTIME, ...(saved.RUNTIME || {}) } };
|
const merged = { ...DEFAULT_CONFIG, ...saved, RUNTIME: { ...DEFAULT_CONFIG.RUNTIME, ...(saved.RUNTIME || {}) } };
|
||||||
|
return merged;
|
||||||
} catch {
|
} catch {
|
||||||
return structuredClone(DEFAULT_CONFIG);
|
return structuredClone(DEFAULT_CONFIG);
|
||||||
}
|
}
|
||||||
|
|
@ -194,35 +192,17 @@
|
||||||
|
|
||||||
_fallbackCopy(text, originalError = null) {
|
_fallbackCopy(text, originalError = null) {
|
||||||
try {
|
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');
|
const ta = document.createElement('textarea');
|
||||||
ta.value = text;
|
ta.value = text;
|
||||||
ta.readOnly = true;
|
ta.style.position = 'fixed';
|
||||||
ta.style.cssText = 'width:100%;height:40vh;font:12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;';
|
ta.style.opacity = '0';
|
||||||
const row = document.createElement('div');
|
document.body.appendChild(ta);
|
||||||
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.focus();
|
||||||
ta.select();
|
ta.select();
|
||||||
this.warn('Clipboard API unavailable; showing manual copy UI', { error: originalError?.message });
|
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)');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.warn('Clipboard copy failed', { error: originalError?.message || e.message });
|
this.warn('Clipboard copy failed', { error: originalError?.message || e.message });
|
||||||
}
|
}
|
||||||
|
|
@ -711,7 +691,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the *command block* if present; else fall back to element text
|
// Extract the *command block* if present; else fall back to element text
|
||||||
function _commandLikeText(el) {
|
function _commandishText(el) {
|
||||||
// Mirror parser's detector: require header, action, and '@end@'
|
// Mirror parser's detector: require header, action, and '@end@'
|
||||||
const blocks = el.querySelectorAll('pre code, pre, code');
|
const blocks = el.querySelectorAll('pre code, pre, code');
|
||||||
for (const b of blocks) {
|
for (const b of blocks) {
|
||||||
|
|
@ -726,7 +706,7 @@
|
||||||
|
|
||||||
// Hash of the command (or element text) capped to 2000 chars
|
// Hash of the command (or element text) capped to 2000 chars
|
||||||
function _hashCommand(el) {
|
function _hashCommand(el) {
|
||||||
const t = _commandLikeText(el);
|
const t = _commandishText(el);
|
||||||
return _hash(t.slice(0, 2000));
|
return _hash(t.slice(0, 2000));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -934,9 +914,8 @@
|
||||||
'create_release': ['action', 'repo', 'tag_name', 'name']
|
'create_release': ['action', 'repo', 'tag_name', 'name']
|
||||||
};
|
};
|
||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
const FIELD_VALIDATORS = {
|
||||||
const FIELD_VALIDATORS = {
|
repo: (v) => /^[\w\-\.]+(\/[\w\-\.]+)?$/.test(v),
|
||||||
repo: (v) => /^[\w\-.]+(\/[\w\-.]+)?$/.test(v),
|
|
||||||
path: (v) => !v.includes('..') && !v.startsWith('/') && !v.includes('\\'),
|
path: (v) => !v.includes('..') && !v.startsWith('/') && !v.includes('\\'),
|
||||||
action: (v) => Object.keys(REQUIRED_FIELDS).includes(v),
|
action: (v) => Object.keys(REQUIRED_FIELDS).includes(v),
|
||||||
url: (v) => !v || /^https?:\/\/[^/\s]+(?:\/|$)/i.test(v),
|
url: (v) => !v || /^https?:\/\/[^/\s]+(?:\/|$)/i.test(v),
|
||||||
|
|
@ -1022,13 +1001,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Element} el
|
|
||||||
* @param {string|number} [suffix]
|
|
||||||
*/
|
|
||||||
hasElement(el, suffix = '') {
|
hasElement(el, suffix = '') {
|
||||||
let fp = fingerprintElement(el);
|
let fp = fingerprintElement(el);
|
||||||
if (suffix !== '' && suffix != null) fp += `#${String(suffix)}`;
|
if (suffix) fp += `#${suffix}`;
|
||||||
const result = this.session.has(fp) || (fp in this.cache);
|
const result = this.session.has(fp) || (fp in this.cache);
|
||||||
|
|
||||||
if (result && CONFIG.DEBUG_LEVEL >= 4) {
|
if (result && CONFIG.DEBUG_LEVEL >= 4) {
|
||||||
|
|
@ -1042,13 +1017,9 @@
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Element} el
|
|
||||||
* @param {string|number} [suffix]
|
|
||||||
*/
|
|
||||||
markElement(el, suffix = '') {
|
markElement(el, suffix = '') {
|
||||||
let fp = fingerprintElement(el);
|
let fp = fingerprintElement(el);
|
||||||
if (suffix !== '' && suffix != null) fp += `#${String(suffix)}`;
|
if (suffix) fp += `#${suffix}`;
|
||||||
this.session.add(fp);
|
this.session.add(fp);
|
||||||
this.cache[fp] = Date.now();
|
this.cache[fp] = Date.now();
|
||||||
this._save();
|
this._save();
|
||||||
|
|
@ -1057,7 +1028,7 @@
|
||||||
fingerprint: fp.slice(0, 60) + '...'
|
fingerprint: fp.slice(0, 60) + '...'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (CONFIG.SHOW_EXECUTED_MARKER && el instanceof HTMLElement) {
|
if (CONFIG.SHOW_EXECUTED_MARKER) {
|
||||||
try {
|
try {
|
||||||
el.style.borderLeft = '3px solid #10B981';
|
el.style.borderLeft = '3px solid #10B981';
|
||||||
el.title = 'Command executed — use "Run again" to re-run';
|
el.title = 'Command executed — use "Run again" to re-run';
|
||||||
|
|
@ -1065,6 +1036,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
resetAll() {
|
||||||
this.session.clear();
|
this.session.clear();
|
||||||
localStorage.removeItem(this.key);
|
localStorage.removeItem(this.key);
|
||||||
|
|
@ -1074,8 +1059,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global helpers (stable)
|
// Global helpers (stable)
|
||||||
// noinspection JSUnusedGlobalSymbols
|
window.AI_REPO = {
|
||||||
window.AI_REPO = {
|
|
||||||
clearHistory: () => {
|
clearHistory: () => {
|
||||||
try { commandMonitor?.history?.resetAll?.(); } catch {}
|
try { commandMonitor?.history?.resetAll?.(); } catch {}
|
||||||
localStorage.removeItem(STORAGE_KEYS.history); // legacy
|
localStorage.removeItem(STORAGE_KEYS.history); // legacy
|
||||||
|
|
@ -1305,7 +1289,7 @@
|
||||||
'form button[type="submit"]'
|
'form button[type="submit"]'
|
||||||
];
|
];
|
||||||
for (const s of selectors) {
|
for (const s of selectors) {
|
||||||
const btn = scope.querySelector(s);
|
const btn = document.querySelector(s);
|
||||||
if (!btn) continue;
|
if (!btn) continue;
|
||||||
const style = window.getComputedStyle(btn);
|
const style = window.getComputedStyle(btn);
|
||||||
const disabled = btn.disabled || btn.getAttribute('aria-disabled') === 'true';
|
const disabled = btn.disabled || btn.getAttribute('aria-disabled') === 'true';
|
||||||
|
|
@ -1385,52 +1369,37 @@
|
||||||
// Pad with blank lines before/after to preserve ``` fences visually.
|
// Pad with blank lines before/after to preserve ``` fences visually.
|
||||||
const payload2 = `\n${payload.replace(/\n?$/, '\n')}\n`;
|
const payload2 = `\n${payload.replace(/\n?$/, '\n')}\n`;
|
||||||
const escape = (s) => s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
const escape = (s) => s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||||
|
|
||||||
el.innerHTML = String(payload2)
|
const html = String(payload2)
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map(line => line.length ? `<p>${escape(line)}</p>` : '<p><br></p>')
|
.map(line => line.length ? `<p>${escape(line)}</p>` : '<p><br></p>')
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
|
el.innerHTML = html;
|
||||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
RC_DEBUG?.info('✅ Paste method succeeded: ProseMirror');
|
RC_DEBUG?.info('✅ Paste method succeeded: ProseMirror');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method 3: Selection API insertion (non-deprecated)
|
// Method 3: execCommand
|
||||||
try {
|
try {
|
||||||
const sel = window.getSelection && window.getSelection();
|
const sel = window.getSelection && window.getSelection();
|
||||||
if (el.isContentEditable || el.getAttribute('contenteditable') === 'true') {
|
if (sel && sel.rangeCount === 0 && el instanceof HTMLElement) {
|
||||||
// Ensure there's a caret; if not, place it at the end
|
const r = document.createRange();
|
||||||
if (sel && sel.rangeCount === 0) {
|
r.selectNodeContents(el); r.collapse(false); sel.removeAllRanges(); sel.addRange(r);
|
||||||
const r = document.createRange();
|
RC_DEBUG?.verbose('Selection range set for execCommand');
|
||||||
r.selectNodeContents(el); r.collapse(false); sel.removeAllRanges(); sel.addRange(r);
|
}
|
||||||
RC_DEBUG?.verbose('Selection range set for contentEditable');
|
|
||||||
}
|
const success = document.execCommand && document.execCommand('insertText', false, payload);
|
||||||
if (sel && sel.rangeCount > 0) {
|
RC_DEBUG?.verbose('execCommand attempt', { success });
|
||||||
const range = sel.getRangeAt(0);
|
|
||||||
range.deleteContents();
|
if (success) {
|
||||||
const node = document.createTextNode(payload);
|
RC_DEBUG?.info('✅ Paste method succeeded: execCommand');
|
||||||
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;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
RC_DEBUG?.verbose('Selection API insertion failed', { error: String(e) });
|
RC_DEBUG?.verbose('execCommand failed', { error: String(e) });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method 4: TEXTAREA/INPUT
|
// Method 4: TEXTAREA/INPUT
|
||||||
|
|
@ -1625,7 +1594,8 @@
|
||||||
|
|
||||||
if (!CONFIG.ENABLE_API) {
|
if (!CONFIG.ENABLE_API) {
|
||||||
UIFeedback.appendStatus(sourceElement, 'EXECUTING', { action: command.action, details: 'Mocking...', key: renderKey, label });
|
UIFeedback.appendStatus(sourceElement, 'EXECUTING', { action: command.action, details: 'Mocking...', key: renderKey, label });
|
||||||
return await this.mockExecution(command, sourceElement, renderKey, label);
|
const res = await this.mockExecution(command, sourceElement, renderKey, label);
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
UIFeedback.appendStatus(sourceElement, 'EXECUTING', { action: command.action, details: 'Making API request...', key: renderKey, label });
|
UIFeedback.appendStatus(sourceElement, 'EXECUTING', { action: command.action, details: 'Making API request...', key: renderKey, label });
|
||||||
|
|
@ -1743,7 +1713,7 @@
|
||||||
if (command.action === 'get_file') {
|
if (command.action === 'get_file') {
|
||||||
const body = this._extractGetFileBody(data);
|
const body = this._extractGetFileBody(data);
|
||||||
if (typeof body === 'string' && body.length) {
|
if (typeof body === 'string' && body.length) {
|
||||||
new ResponseBuffer().push({ label, content: body });
|
RESP_BUFFER.push({ label, content: body });
|
||||||
} else {
|
} else {
|
||||||
GM_notification({ title: 'AI Repo Commander', text: 'get_file succeeded, but no content to paste.', timeout: 4000 });
|
GM_notification({ title: 'AI Repo Commander', text: 'get_file succeeded, but no content to paste.', timeout: 4000 });
|
||||||
}
|
}
|
||||||
|
|
@ -1753,10 +1723,10 @@
|
||||||
const files = this._extractFilesArray(data);
|
const files = this._extractFilesArray(data);
|
||||||
if (files && files.length) {
|
if (files && files.length) {
|
||||||
const listing = this._formatFilesListing(files);
|
const listing = this._formatFilesListing(files);
|
||||||
new ResponseBuffer().push({ label, content: listing });
|
RESP_BUFFER.push({ label, content: listing });
|
||||||
} else {
|
} else {
|
||||||
const fallback = '```json\n' + JSON.stringify(data, null, 2) + '\n```';
|
const fallback = '```json\n' + JSON.stringify(data, null, 2) + '\n```';
|
||||||
new ResponseBuffer().push({ label, content: fallback });
|
RESP_BUFFER.push({ label, content: fallback });
|
||||||
GM_notification({
|
GM_notification({
|
||||||
title: 'AI Repo Commander',
|
title: 'AI Repo Commander',
|
||||||
text: 'list_files succeeded, but response had no obvious files array. Pasted raw JSON.',
|
text: 'list_files succeeded, but response had no obvious files array. Pasted raw JSON.',
|
||||||
|
|
@ -1794,7 +1764,7 @@
|
||||||
push(task) {
|
push(task) {
|
||||||
this.q.push(task);
|
this.q.push(task);
|
||||||
this.onSizeChange?.(this.q.length);
|
this.onSizeChange?.(this.q.length);
|
||||||
if (!this.running) void this._drain();
|
if (!this.running) this._drain();
|
||||||
}
|
}
|
||||||
clear() {
|
clear() {
|
||||||
this.q.length = 0;
|
this.q.length = 0;
|
||||||
|
|
@ -1829,8 +1799,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const execQueue = new ExecutionQueue();
|
const execQueue = new ExecutionQueue();
|
||||||
// noinspection JSUnusedGlobalSymbols
|
window.AI_REPO_QUEUE = {
|
||||||
window.AI_REPO_QUEUE = {
|
|
||||||
clear: () => execQueue.clear(),
|
clear: () => execQueue.clear(),
|
||||||
size: () => execQueue.q.length,
|
size: () => execQueue.q.length,
|
||||||
cancelOne: (cb) => execQueue.cancelOne(cb),
|
cancelOne: (cb) => execQueue.cancelOne(cb),
|
||||||
|
|
@ -1941,8 +1910,8 @@
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RESP_BUFFER = new ResponseBuffer();
|
||||||
window.AI_REPO_RESPONSES = new ResponseBuffer(); // optional debug handle
|
window.AI_REPO_RESPONSES = RESP_BUFFER; // optional debug handle
|
||||||
|
|
||||||
// ---------------------- Bridge Key ----------------------
|
// ---------------------- Bridge Key ----------------------
|
||||||
let BRIDGE_KEY = null;
|
let BRIDGE_KEY = null;
|
||||||
|
|
@ -2260,6 +2229,8 @@
|
||||||
let parsed;
|
let parsed;
|
||||||
try {
|
try {
|
||||||
parsed = CommandParser.parseYAMLCommand(finalTxt);
|
parsed = CommandParser.parseYAMLCommand(finalTxt);
|
||||||
|
const val = CommandParser.validateStructure(parsed);
|
||||||
|
if (!val.isValid) throw new Error(`Validation failed: ${val.errors.join(', ')}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
UIFeedback.appendStatus(element, 'ERROR', {
|
UIFeedback.appendStatus(element, 'ERROR', {
|
||||||
action: 'Command',
|
action: 'Command',
|
||||||
|
|
@ -2270,17 +2241,6 @@
|
||||||
this.attachRetryUI(element, subId);
|
this.attachRetryUI(element, subId);
|
||||||
return;
|
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);
|
this.updateState(subId, COMMAND_STATES.EXECUTING);
|
||||||
const res = await ExecutionManager.executeCommand(
|
const res = await ExecutionManager.executeCommand(
|
||||||
|
|
@ -2325,7 +2285,7 @@
|
||||||
cancelToken: { cancelled: false }
|
cancelToken: { cancelled: false }
|
||||||
});
|
});
|
||||||
this.updateState(messageId, COMMAND_STATES.PARSING);
|
this.updateState(messageId, COMMAND_STATES.PARSING);
|
||||||
void this.processCommand(messageId);
|
this.processCommand(messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async debounceWithCancel(messageId) {
|
async debounceWithCancel(messageId) {
|
||||||
|
|
@ -2432,8 +2392,8 @@
|
||||||
// Highlight failed one
|
// Highlight failed one
|
||||||
try {
|
try {
|
||||||
const bar = element.querySelector('.ai-rc-rerun');
|
const bar = element.querySelector('.ai-rc-rerun');
|
||||||
const buttons = [...bar.querySelectorAll('button')].filter(b => /Run again \[#\d+]/.test(b.textContent || ''));
|
const btns = [...bar.querySelectorAll('button')].filter(b => /Run again \[#\d+\]/.test(b.textContent || ''));
|
||||||
const b = buttons[failedIdx];
|
const b = btns[failedIdx];
|
||||||
if (b) b.style.outline = '2px solid #ef4444';
|
if (b) b.style.outline = '2px solid #ef4444';
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
@ -2507,13 +2467,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validation.isValid) {
|
if (!validation.isValid) {
|
||||||
this.updateState(messageId, COMMAND_STATES.ERROR);
|
|
||||||
this.attachRetryUI(message.element, messageId);
|
this.attachRetryUI(message.element, messageId);
|
||||||
UIFeedback.appendStatus(message.element, 'ERROR', {
|
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
|
||||||
action: 'Command',
|
|
||||||
details: `Validation failed: ${validation.errors.join(', ')}`
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Debounce
|
// 3) Debounce
|
||||||
|
|
@ -2569,13 +2524,8 @@
|
||||||
const reParsed = CommandParser.parseYAMLCommand(stable);
|
const reParsed = CommandParser.parseYAMLCommand(stable);
|
||||||
const reVal = CommandParser.validateStructure(reParsed);
|
const reVal = CommandParser.validateStructure(reParsed);
|
||||||
if (!reVal.isValid) {
|
if (!reVal.isValid) {
|
||||||
this.updateState(messageId, COMMAND_STATES.ERROR);
|
|
||||||
this.attachRetryUI(message.element, messageId);
|
this.attachRetryUI(message.element, messageId);
|
||||||
UIFeedback.appendStatus(message.element, 'ERROR', {
|
throw new Error(`Final validation failed: ${reVal.errors.join(', ')}`);
|
||||||
action: 'Command',
|
|
||||||
details: `Final validation failed: ${reVal.errors.join(', ')}`
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
parsed = reParsed;
|
parsed = reParsed;
|
||||||
}
|
}
|
||||||
|
|
@ -2697,7 +2647,7 @@
|
||||||
|
|
||||||
RC_DEBUG?.info('Retrying message now', { messageId });
|
RC_DEBUG?.info('Retrying message now', { messageId });
|
||||||
commandMonitor.updateState(messageId, COMMAND_STATES.PARSING);
|
commandMonitor.updateState(messageId, COMMAND_STATES.PARSING);
|
||||||
void commandMonitor.processCommand(messageId);
|
commandMonitor.processCommand(messageId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
RC_DEBUG?.error('Failed to retry message', { messageId, error: String(e) });
|
RC_DEBUG?.error('Failed to retry message', { messageId, error: String(e) });
|
||||||
}
|
}
|
||||||
|
|
@ -2843,8 +2793,7 @@ path: .
|
||||||
|
|
||||||
if (!commandMonitor) {
|
if (!commandMonitor) {
|
||||||
commandMonitor = new CommandMonitor();
|
commandMonitor = new CommandMonitor();
|
||||||
// noinspection JSUnusedGlobalSymbols
|
window.AI_REPO_COMMANDER = {
|
||||||
window.AI_REPO_COMMANDER = {
|
|
||||||
monitor: commandMonitor,
|
monitor: commandMonitor,
|
||||||
config: CONFIG,
|
config: CONFIG,
|
||||||
test: TEST_COMMANDS,
|
test: TEST_COMMANDS,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue