diff --git a/Docs/Technical_Design_Document.md b/Docs/Technical_Design_Document.md index e06321b..a41fcf9 100644 --- a/Docs/Technical_Design_Document.md +++ b/Docs/Technical_Design_Document.md @@ -4,6 +4,14 @@ 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 diff --git a/README.md b/README.md index 0b1aa9f..cf5301c 100644 --- a/README.md +++ b/README.md @@ -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. It’s silently skipped (no error UI). diff --git a/src/ai-repo-commander.user.js b/src/ai-repo-commander.user.js index db6076d..4f54af0 100644 --- a/src/ai-repo-commander.user.js +++ b/src/ai-repo-commander.user.js @@ -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,'&').replace(//g,'>'); - - const html = String(payload2) - .split('\n') - .map(line => line.length ? `
${escape(line)}
` : '${escape(line)}
` : '