From c408289e0fb820016432aa5ad65e065527c030bd Mon Sep 17 00:00:00 2001 From: rob Date: Thu, 16 Oct 2025 17:32:09 -0300 Subject: [PATCH] added comments --- Docs/diagrams/config.js.class.puml | 90 +++++ Docs/diagrams/config.js.unified_views.puml | 445 +++++++++++++++++++++ Docs/diagrams/state-machine.puml | 42 -- src/command-executor.js | 5 + src/command-parser.js | 15 + src/debug-panel.js | 6 + src/detector.js | 8 + src/fingerprint-strong.js | 83 +++- src/main.js | 6 + src/paste-submit.js | 8 + src/queue.js | 12 + src/response-buffer.js | 12 + src/storage.js | 10 + src/userscript-bootstrap.user.js | 1 + 14 files changed, 700 insertions(+), 43 deletions(-) create mode 100644 Docs/diagrams/config.js.class.puml create mode 100644 Docs/diagrams/config.js.unified_views.puml delete mode 100644 Docs/diagrams/state-machine.puml diff --git a/Docs/diagrams/config.js.class.puml b/Docs/diagrams/config.js.class.puml new file mode 100644 index 0000000..229c96a --- /dev/null +++ b/Docs/diagrams/config.js.class.puml @@ -0,0 +1,90 @@ +@startuml +title AI Repo Commander — Config overview (safe) + +class ConfigManager { + +load() : object + +save() : void + +get(keyPath) : any + +set(keyPath, value) : void + +mergeConfigs(defaults, saved) : object + +deepClone(o) : any +} + +class STORAGE_KEYS { + history : "ai_repo_commander_executed" + cfg : "ai_repo_commander_cfg" + panel : "ai_repo_commander_panel_state" +} + +' Split DEFAULT_CONFIG into simple classes (no quotes, no =) +class DEFAULT_META { + version : 1.6.2 +} +class DEFAULT_API { + enabled : boolean + timeout : number + maxRetries : number + bridgeKey : string +} +class DEFAULT_DEBUG { + enabled : boolean + level : number + maxLines : number + showPanel : boolean +} +class DEFAULT_EXECUTION { + debounceDelay : ms + settleCheckMs : ms + settlePollMs : ms + requireTerminator : boolean + coldStartMs : ms + stuckAfterMs : ms + scanDebounceMs : ms + fastWarnMs : ms + slowWarnMs : ms + clusterRescanMs : ms + clusterMaxLookahead : count +} +class DEFAULT_QUEUE { + minDelayMs : ms + maxPerMinute : count + maxPerMessage : count + waitForComposerMs : ms +} +class DEFAULT_UI { + autoSubmit : boolean + appendTrailingNewline : boolean + postPasteDelayMs : ms + showExecutedMarker : boolean + processExisting : boolean + submitMode : "enter|ctrl+enter" + maxComposerWaitMs : ms + submitMaxRetries : count +} +class DEFAULT_STORAGE { + dedupeTtlMs : ms + cleanupAfterMs : ms + cleanupIntervalMs : ms +} +class DEFAULT_RESPONSE { + bufferFlushDelayMs : ms + sectionHeadings : boolean + maxPasteChars : number + splitLongResponses : boolean +} +class DEFAULT_RUNTIME { + paused : boolean + '(note: runtime is not persisted) +} + +ConfigManager --> STORAGE_KEYS : uses localStorage keys +ConfigManager --> DEFAULT_META : deepClone/merge +ConfigManager --> DEFAULT_API +ConfigManager --> DEFAULT_DEBUG +ConfigManager --> DEFAULT_EXECUTION +ConfigManager --> DEFAULT_QUEUE +ConfigManager --> DEFAULT_UI +ConfigManager --> DEFAULT_STORAGE +ConfigManager --> DEFAULT_RESPONSE +ConfigManager --> DEFAULT_RUNTIME : in-memory only +@enduml diff --git a/Docs/diagrams/config.js.unified_views.puml b/Docs/diagrams/config.js.unified_views.puml new file mode 100644 index 0000000..2c9cf16 --- /dev/null +++ b/Docs/diagrams/config.js.unified_views.puml @@ -0,0 +1,445 @@ +' =================================================================== +' File: ConfigManager.puml +' Purpose: Single source of truth for class-level activity and per-method sequences. +' Edit rules: Follow the legend at bottom; preserve VIEW/METHOD anchors for automation. +' =================================================================== + +' (Optional) neutral defaults — typography/layout only (keeps your colors intact) +skinparam Shadowing false +skinparam SequenceMessageAlign center +skinparam SequenceLifeLineBorderColor #666666 +skinparam SequenceLifeLineBorderThickness 1 + +' ==== VIEW: Branch Flow (full class) ========================================== +@startuml +title ConfigManager — Branch Flow (full class) + +start +:ConfigManager; + +' Fan-out to each method +fork + ' -------- constructor() -------- + partition "constructor()" #E7FAE3 { + :constructor(); + :this.config = load(); + kill + } +fork again + ' -------- load() -------- + partition "load()" #FFF6D1 { + :load(); + :raw = localStorage.getItem(STORAGE_KEYS.cfg); + if (raw is null/empty?) then (yes) + :config = deepClone(DEFAULT_CONFIG); + kill + else (no) + :try parse = JSON.parse(raw); + if (parse ok?) then (yes) + :saved = parse; + :config = mergeConfigs(DEFAULT_CONFIG, saved); + kill + else (no / parse error) + :config = deepClone(DEFAULT_CONFIG); + kill + endif + endif + } +fork again + ' -------- save() -------- + partition "save()" #FFE1DB { + :save(); + :persistable = deepClone(config); + if (persistable has runtime?) then (yes) + :delete persistable.runtime; + endif + :try json = JSON.stringify(persistable); + if (stringify ok?) then (yes) + :localStorage.setItem(STORAGE_KEYS.cfg, json); + kill + else (no / stringify error) + :/* log/notify failure */; + kill + endif + } +fork again + ' -------- get(keyPath) -------- + partition "get(keyPath)" #DCF9EE { + :get(keyPath); + :parts = keyPath.split('.'); + :node = config; + while (more parts?) + :p = next part; + if (node[p] exists?) then (yes) + :node = node[p]; + else (no) + :return undefined; + kill + endif + endwhile + :return node; + kill + } +fork again + ' -------- set(keyPath, value) -------- + partition "set(keyPath, value)" #FFE6F0 { + :set(keyPath, value); + :parts = keyPath.split('.'); + :node = config; + while (more parts?) + :p = next part; + if (node[p] exists?) then (yes) + :node = node[p]; + else (no) + :node[p] = {}; + :node = node[p]; + endif + endwhile + :assign value at final key; + :save(); + kill + } +fork again + ' -------- mergeConfigs(defaults, saved) -------- + partition "mergeConfigs(defaults, saved)" #E6F3FF { + :mergeConfigs(defaults, saved); + :result = deepClone(defaults); + if (saved is object?) then (yes) + :for each key k in saved; + while (keys left?) + :k = next key; + if (k == "runtime"?) then (yes) + :skip; + else (no) + if (both result[k] and saved[k] are plain objects?) then (yes) + :result[k] = mergeConfigs(result[k], saved[k]); + else (no) + :result[k] = deepClone(saved[k]); + endif + endif + endwhile + else (no) + :/* nothing to merge */; + endif + :return result; + kill + } +fork again + ' -------- deepClone(o) -------- + partition "deepClone(o)" #F0E6FA { + :deepClone(o); + if (o is null or primitive?) then (yes) + :return o; + kill + else (no) + if (o is Array?) then (yes) + :clone = []; + :for each item -> push( deepClone(item) ); + :return clone; + kill + else (no) + :clone = {}; + :for each key -> clone[key] = deepClone(o[key]); + :return clone; + kill + endif + endif + } + end fork +@enduml + +' ==== METHOD: constructor() ================================================ +@startuml +title ConfigManager:constructor(): \n Populate this.config at instantiation + +actor Page as PG +participant "constructor()" as CTOR +participant "load()" as LD + +PG -> CTOR : new ConfigManager() +activate CTOR +CTOR -> LD : populate this.config +LD --> CTOR : config object +CTOR --> PG : this.config set +deactivate CTOR +@enduml + +' ==== METHOD: load() ======================================================= +@startuml +title ConfigManager:load(): \n Read from localStorage, parse+merge or fallback to defaults + +participant "Caller" as CL +participant "load()" as LD +participant "localStorage" as LS +participant "mergeConfigs()" as MC +participant "deepClone()" as DC + +activate CL +activate LD +CL -> LD : initial request +activate LS +LD -> LS : getItem(STORAGE_KEYS.cfg) +LS --> LD : getItem(STORAGE_KEYS.cfg) +deactivate LS +alt STORAGE_KEYS.cfg (Empty) +activate DC + LD -> DC : deepClone(DEFAULT_CONFIG) + LD <-- DC : defaults clone +deactivate DC + LD --> CL : return defaults +else STORAGE_KEYS.cfg (Not empty) + LD --> LD : try parse STORAGE_KEYS.cfg + alt parse ok + LD --> LD : saved = parsed + activate MC + LD -> MC : mergeConfigs(DEFAULT_CONFIG, saved) + MC --> LD : merged config + deactivate MC + LD --> CL : return merged config + else parse error + activate DC + LD -> DC : deepClone(DEFAULT_CONFIG) + LD <-- DC : defaults clone + deactivate DC + LD --> CL : return defaults + end +end +@enduml + +' ==== METHOD: save() ======================================================= +@startuml +title ConfigManager:save(): \n Strip runtime, stringify, persist to localStorage + +participant "Caller" as CL +participant "save()" as SV +participant "deepClone()" as DC +participant "JSON" as JS +participant "localStorage" as LS +participant "Console" as CLG + +activate CL +CL -> SV : initial request +deactivate CL +activate SV +activate DC +SV -> DC : deepClone(config) +DC --> SV : persistable clone +deactivate DC + +SV -> SV : delete any persistable.runtime +activate JS +SV -> JS : JSON.stringify(persistable) +alt stringify ok + JS --> SV : json string + activate LS + SV -> LS : setItem(STORAGE_KEYS.cfg, json) + deactivate LS +else stringify error + JS --> SV : error + activate CLG + SV -> CLG : log/notify failure + deactivate CLG +end +deactivate JS +@enduml + +' ==== METHOD: get(keyPath) ================================================ +@startuml +title ConfigManager:get(keyPath): \n Resolve a dotted path or return undefined + +participant "Caller" as CL +participant "get(keyPath)" as GET + +activate CL +CL -> GET : initial request (keyPath) +activate GET + +GET -> GET : parts = keyPath.split('.'); node = config +loop for each part + alt node has part + GET -> GET : node = node[part] + else missing segment + GET --> CL : undefined + end +end + +GET --> CL : value (final node) +deactivate GET +deactivate CL +@enduml + +' ==== METHOD: set(keyPath, value) ========================================= +@startuml +title ConfigManager:set(keyPath, value): \n Create missing path segments, assign, then persist + +participant "Caller" as CL +participant "set(keyPath, value)" as SET +participant "save()" as SV + +activate CL +CL -> SET : initial request (keyPath, value) +activate SET + +SET -> SET : parts = keyPath.split('.'); node = config +loop for each part (except last) + alt node has part + SET -> SET : node = node[part] + else missing + SET -> SET : node[part] = {}; node = node[part] + end +end + +SET -> SET : assign value at final key +SET -> SV : save() +SV --> SET : persisted + +SET --> CL : done +deactivate SET +deactivate CL +@enduml + +' ==== METHOD: mergeConfigs(defaults, saved) ================================ +@startuml +title ConfigManager:mergeConfigs(defaults, saved): \n Deep merge saved over defaults (skip runtime) + +participant "Caller" as CL +participant "mergeConfigs()" as MC +participant "deepClone()" as DC + +activate CL +CL -> MC : initial request (defaults, saved) +activate MC + +MC -> DC : deepClone(defaults) +DC --> MC : result (clone of defaults) + +alt saved is plain object + loop for each key k in saved + alt k == "runtime" + MC -> MC : skip key + else not runtime + alt both result[k] and saved[k] are plain objects + MC -> MC : result[k] = mergeConfigs(result[k], saved[k]) ' recursive + else overwrite + MC -> DC : deepClone(saved[k]) + DC --> MC : cloned value + MC -> MC : result[k] = cloned value + end + end + end +else saved not object + MC -> MC : nothing to merge +end + +MC --> CL : result +deactivate MC +deactivate CL +@enduml + +' ==== METHOD: deepClone(o) ================================================ +@startuml +title ConfigManager:deepClone(o): \n Structural clone for arrays/objects; primitives by value + +participant "Caller" as CL +participant "deepClone()" as DC + +activate CL +CL -> DC : initial request (o) +activate DC + +alt o is null or primitive + DC --> CL : o + deactivate DC + deactivate CL + return +else non-primitive + alt Array + DC -> DC : clone = [] + loop each item + DC -> DC : clone.push( deepClone(item) ) + end + DC --> CL : clone + else Object + DC -> DC : clone = {} + loop each key + DC -> DC : clone[key] = deepClone(o[key]) + end + DC --> CL : clone + end +end + +deactivate DC +deactivate CL +@enduml + +' ==== LEGEND =============================================================== +@startuml +legend bottom +== Config UML Style Guide (for future edits) == +• Scope: One .puml per class or file. Keep two views: + (1) Activity "Branch Flow" for all methods for the class or functions in the file. + - (partitions + soft colors), + (2) Per-method Sequence diagrams for each of the methods or functions. + +• Sequence conventions: + 1) First participant is the external caller (use "Caller" or "Page"). + 2) Do NOT add the class lifeline unless needed (constructor). Class name appears in title only. + 3) Include every directly-called method or subsystem as a participant + (e.g., "load()", "mergeConfigs()", "deepClone()", "JSON", "localStorage", "Console"). + 4) Prefer simple messages. + 5) Use activate/deactivate for the method under focus and key collaborators. + 6) Use alt blocks only when branches meaningfully change the message flow. + For trivial checks (e.g., delete runtime if exists), inline the action. + 7) Titles: "ClassName:method(): \n Detailed description of the flow". + +• Activity view conventions: + A) Start with Class(or filename) node then fork partitions for each method or function. + B) One partition per method; soft background color; terminate branches with 'kill'. + C) Keep wording aligned with code (e.g., "deepClone(DEFAULT_CONFIG)", "mergeConfigs(...)"). + +• Color palette (soft pastels) +• Use --> for returns; -> for calls. +• Participants use quoted method names for internals (e.g., "save()"), and plain nouns for systems ("JSON", "localStorage", "Console"). +• When modifying this file keep this legend at the end of the file to standardize edits. + +UML_Example +------------------------------------------ +title ClassName:methodName(args): \n Detailed description of what this method does + +participant "Caller" as CL +participant "methodName()" as M ' the method under focus +' Add collaborators as needed: +' participant "SomeDependency" as DEP +' participant "AnotherMethod()" as AM +' participant "JSON" as JS +' participant "localStorage" as LS + +activate CL +CL -> M : initial request (args) +activate M +' -- inner flow (keep alt blocks only if they clarify) -- +' Example pattern: +activate LS +' M -> LS : getItem(KEY) +' LS --> M : value +deactivate LS + +' alt branch condition +activate AM +' M -> AM : call anotherMethod(...) +' AM --> M : result +deactivate AM +' else other branch +activate DEP +' M -> DEP : do something +' DEP -> M : ok +deactivate DEP +' end + +' Return to caller +M -> CL : return value +deactivate M +deactivate CL +--------------------------------------------- +endlegend +@enduml diff --git a/Docs/diagrams/state-machine.puml b/Docs/diagrams/state-machine.puml deleted file mode 100644 index 37d633a..0000000 --- a/Docs/diagrams/state-machine.puml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/command-executor.js b/src/command-executor.js index 69f9363..98e45cd 100644 --- a/src/command-executor.js +++ b/src/command-executor.js @@ -1,4 +1,9 @@ // ==COMMAND EXECUTOR START== +// Module: command-executor.js +// Depends on: config.js, logger.js +// Purpose: Execute validated repo commands via the bridge API (GM_xmlhttpRequest), +// handling retries/timeouts and rendering success/error effects. Stores large +// response bodies for paste-submit. /* global GM_xmlhttpRequest */ /* global GM_notification */ (function () { diff --git a/src/command-parser.js b/src/command-parser.js index f35b25a..2e4a579 100644 --- a/src/command-parser.js +++ b/src/command-parser.js @@ -1,5 +1,20 @@ // ==COMMAND PARSER START== +// Module: command-parser.js +// Purpose: Extract and parse YAML-like command blocks embedded in assistant messages. +// - Looks for a complete block delimited by @bridge@ ... @end@ +// - Parses simple key/value pairs and multiline "|" blocks +// - Applies sane defaults (url, owner, source_branch) +// - Validates presence of required fields per action +// This module is side-effect free and exposes a single class via window.AI_REPO_PARSER. (function () { + /** + * CommandParser + * - parse(text): Extracts first complete command block and returns a structured object + * - extractBlock(text): Returns the inner text between @bridge@ and @end@ + * - parseKV(block): Minimal YAML-like parser supporting multi-line values with "|" + * - applyDefaults(obj): Applies default values and owner/repo split logic + * - validate(obj): Returns { isValid, errors, example? } + */ class CommandParser { static REQUIRED = { get_file: ['action', 'repo', 'path'], diff --git a/src/debug-panel.js b/src/debug-panel.js index 8af73cb..f42d340 100644 --- a/src/debug-panel.js +++ b/src/debug-panel.js @@ -1,5 +1,11 @@ // ==DEBUG PANEL START== +// Module: debug-panel.js // Depends on: config.js, logger.js, queue.js, storage.js +// Purpose: In-page draggable panel showing recent logs and exposing tools/settings. +// - Logs tab: tail of the Logger buffer with copy buttons +// - Tools & Settings: toggles and numeric inputs bound to config, quick actions +// - Pause/Stop controls and queue size indicator +// The panel stores its position/collapsed state in localStorage (see config.js STORAGE_KEYS). /* global GM_notification */ (function () { const cfg = () => window.AI_REPO_CONFIG; diff --git a/src/detector.js b/src/detector.js index f243f4a..91bed48 100644 --- a/src/detector.js +++ b/src/detector.js @@ -1,5 +1,13 @@ // ==DETECTOR START== +// Module: detector.js // Depends on: config.js, logger.js, queue.js, command-parser.js, command-executor.js, storage.js +// Purpose: Observe assistant messages in the DOM, wait for streaming to finish, +// extract @bridge@...@end@ command blocks, and enqueue them for execution. +// Key features: +// - Debounce window to allow AI streaming to complete +// - Settle check: ensures text remains stable for a small window +// - Per-message command cap and de-duplication via storage.js +// - Optional processing of existing messages on page load (function () { const cfg = () => window.AI_REPO_CONFIG; const log = () => window.AI_REPO_LOGGER; diff --git a/src/fingerprint-strong.js b/src/fingerprint-strong.js index eee9145..3e552d4 100644 --- a/src/fingerprint-strong.js +++ b/src/fingerprint-strong.js @@ -1,23 +1,72 @@ // ==FINGERPRINT (drop-in utility) == +// Purpose: Generate a strong, stable fingerprint string for an assistant message element. +// +// This utility is designed to uniquely identify assistant messages across dynamic UIs +// (ChatGPT, Claude, Gemini). It uses multiple content- and position-aware components: +// - ch: content hash of the most "command-like" text in the element (prefers fenced code) +// - ph: hash of the immediately preceding assistant messages' trailing text (context) +// - ih: hash of the text right before the first @bridge@...@end@ block (intra-message prefix) +// - hint: small DOM hint (tag#id.class) hashed to keep size small +// - n: ordinal among elements with same ch+ph+ih on the page at this moment +// +// The combination helps disambiguate near-duplicate messages, re-ordered DOM, and small edits. +// Fingerprints are stable enough to persist in localStorage for de-duplication. +// +// Notes: +// - All hashes are short base36 strings derived from a djb2-xor style hash; fast and sufficient. +// - Inputs are normalized (trim, strip zero-width spaces, normalize whitespace before newlines). +// - The algorithm intentionally looks at at most ~2000 chars per slice for performance. +// - The module is side-effect free except for optional caching of a stable fingerprint in dataset. (function(){ + /** + * CSS selectors that identify assistant messages across supported sites. + * These are joined with "," and used for querySelectorAll when scanning neighbors. + */ const MSG_SELECTORS = [ '[data-message-author-role="assistant"]', '.chat-message:not([data-message-author-role="user"])', '.message-content' ]; + /** + * Normalize text by removing carriage returns and zero-width spaces, squashing trailing + * whitespace before newlines, and trimming ends. Keeps a consistent basis for hashing. + * @param {string} s + * @returns {string} + */ function norm(s){ return (s||'').replace(/\r/g,'').replace(/\u200b/g,'').replace(/[ \t]+\n/g,'\n').trim(); } + + /** + * Fast, low-collision string hash. djb2 variant using XOR; returns unsigned base36 string. + * @param {string} s + * @returns {string} + */ function hash(s){ let h=5381; for(let i=0;i>>0).toString(36); } + /** + * Extract the most relevant, command-like text from a message element. + * Preference order: + * - Any code/pre blocks that appear to contain a valid @bridge@ ... @end@ command with `action:` + * - Otherwise, fall back to the element's textContent (first 2000 chars), normalized. + * @param {Element} el + * @returns {string} + */ function commandLikeText(el){ const blocks = el.querySelectorAll('pre code, pre, code'); for (const b of blocks) { const t = norm(b.textContent || ''); + // Must look like a complete runnable command block if (/@end@\s*$/m.test(t) && /(^|\n)\s*@bridge@\b/m.test(t) && /(^|\n)\s*action\s*:/m.test(t)) return t; } return norm((el.textContent || '').slice(0, 2000)); } + /** + * Build a hash of the immediate previous assistant messages' trailing text (up to 2000 chars). + * This captures conversational context that helps distinguish repeated content. + * @param {Element} el - The current message element + * @returns {string} base36 hash of the context window + */ function prevContextHash(el) { const list = Array.from(document.querySelectorAll(MSG_SELECTORS.join(','))); const idx = list.indexOf(el); if (idx <= 0) return '0'; @@ -29,6 +78,12 @@ return hash(buf.slice(-2000)); } + /** + * Compute a hash of the text immediately preceding the first command block within this element. + * If there is no @bridge@ block, hashes the trailing slice of the whole element text. + * @param {Element} el + * @returns {string} + */ function intraPrefixHash(el){ const t = el.textContent || ''; const m = t.match(/@bridge@[\s\S]*?@end@/m); @@ -36,6 +91,12 @@ return hash(norm(t.slice(Math.max(0, endIdx - 2000), endIdx))); } + /** + * Produce a tiny DOM hint string (tag#id.class) to help separate identical content in different + * containers. Limited to 40 chars and later hashed before inclusion in the final fingerprint. + * @param {Element} node + * @returns {string} + */ function domHint(node) { if (!node) return ''; const id = node.id || ''; @@ -43,6 +104,14 @@ return `${node.tagName || ''}#${id}.${cls}`.slice(0, 40); } + /** + * Determine the ordinal index (0-based) of this element among all message elements that share + * the same content/context key on the page. This disambiguates duplicates that have identical + * ch+ph+ih values by adding their order of appearance. + * @param {Element} el + * @param {string} key - The key built from ch|ph|ih for this element + * @returns {number} + */ function ordinalForKey(el, key) { const list = Array.from(document.querySelectorAll(MSG_SELECTORS.join(','))); let n = 0; @@ -61,6 +130,12 @@ return n; } + /** + * Generate a composite fingerprint for the given message element. + * Format: "ch:<..>|ph:<..>|ih:<..>|hint:<..>|n:" + * @param {Element} el + * @returns {string} + */ function fingerprintElement(el){ const ch = hash(commandLikeText(el).slice(0, 2000)); const ph = prevContextHash(el); @@ -71,6 +146,12 @@ return `${key}|hint:${dh}|n:${n}`; } + /** + * Retrieve (and cache) a stable fingerprint for the element. The first time it's computed, + * the value is stored in el.dataset.aiRcStableFp so subsequent calls don't recompute. + * @param {Element} el + * @returns {string} + */ function getStableFingerprint(el) { if (el?.dataset?.aiRcStableFp) return el.dataset.aiRcStableFp; const fp = fingerprintElement(el); @@ -78,7 +159,7 @@ return fp; } - // Expose both for backward compatibility + // Expose both for backward compatibility with older modules that expect these globals window.AI_REPO_FINGERPRINT = fingerprintElement; window.AI_REPO_STABLE_FINGERPRINT = getStableFingerprint; })(); diff --git a/src/main.js b/src/main.js index a8c08d3..f1cf4ce 100644 --- a/src/main.js +++ b/src/main.js @@ -1,4 +1,10 @@ // ==MAIN START== +// Module: main.js +// Purpose: Legacy entry point and convenience API exposure. +// - Initializes observer and optionally scans existing messages +// - Exposes window.AI_REPO with pause/resume/clearHistory helpers +// Note: detector.js implements the primary monitoring pipeline; this module +// remains for compatibility and console convenience. (function () { 'use strict'; diff --git a/src/paste-submit.js b/src/paste-submit.js index f9d8a73..f4b214b 100644 --- a/src/paste-submit.js +++ b/src/paste-submit.js @@ -1,5 +1,13 @@ // ==PASTE SUBMIT START== +// Module: paste-submit.js // Depends on: config.js, logger.js +// Purpose: Paste text into the site's chat composer (ChatGPT/Claude/Gemini styles) +// and optionally auto-submit. Provides multiple robust strategies for editors. +// Flow: +// - findComposer(): locate the editable area +// - pasteInto(el, text): try a series of paste strategies from cleanest to fallback +// - findSendButton(scopeEl): locate a send/submit button near the editor +// - submitToComposer(text): paste and, if enabled, click Send or press Enter /* global GM_setClipboard */ (function () { const cfg = () => window.AI_REPO_CONFIG; diff --git a/src/queue.js b/src/queue.js index f543374..e8bf4b3 100644 --- a/src/queue.js +++ b/src/queue.js @@ -1,6 +1,18 @@ // ==QUEUE START== +// Module: queue.js // Depends on: config.js, logger.js +// Purpose: Rate-limited FIFO queue to serialize command execution. +// - Enforces min delay between tasks (queue.minDelayMs) +// - Caps tasks started per rolling minute (queue.maxPerMinute) +// - Drains asynchronously; push is fire-and-forget (function () { + /** + * ExecutionQueue + * push(task): enqueue an async function to run later + * clear(): drop all pending tasks + * size(): return queue length + * Internals: _drain() runs tasks while respecting rate limits + */ class ExecutionQueue { constructor(opts = {}) { const cfg = window.AI_REPO_CONFIG; diff --git a/src/response-buffer.js b/src/response-buffer.js index 1cc049f..4ff72e9 100644 --- a/src/response-buffer.js +++ b/src/response-buffer.js @@ -1,6 +1,18 @@ // ==RESPONSE BUFFER START== +// Module: response-buffer.js // Depends on: config.js, logger.js, queue.js, paste-submit.js +// Purpose: Collect result chunks (e.g., from get_file/list_files) and paste them +// into the chat composer in a controlled way. +// - Buffers multiple sections briefly to batch them +// - Splits very long payloads while respecting code fences +// - Enqueues paste operations on the ExecutionQueue (function () { + /** + * Split a string by an approximate character limit, preferring to break at newline boundaries. + * @param {string} s + * @param {number} limit + * @returns {string[]} + */ function chunkByLines(s, limit) { const out = []; let start = 0; while (start < s.length) { diff --git a/src/storage.js b/src/storage.js index 7432326..690c1a6 100644 --- a/src/storage.js +++ b/src/storage.js @@ -1,5 +1,15 @@ // ==STORAGE START== +// Module: storage.js +// Purpose: Conversation-aware de-duplication of executed commands using localStorage. +// - Creates a per-conversation key (host + path) to avoid cross-thread pollution +// - Stores fingerprints of message elements (plus command index) with timestamps +// - Provides isProcessed/markProcessed helpers and TTL cleanup (function () { + /** + * ConversationHistory + * Tracks which commands (by message fingerprint + index) have been executed + * in the current conversation. Uses a 30-day TTL for records. + */ class ConversationHistory { constructor() { this.conversationId = this._getConversationId(); diff --git a/src/userscript-bootstrap.user.js b/src/userscript-bootstrap.user.js index aed7f5c..5be2c87 100644 --- a/src/userscript-bootstrap.user.js +++ b/src/userscript-bootstrap.user.js @@ -1,4 +1,5 @@ // ==UserScript== +// Bootstrap loader: this userscript pulls in all modules via @require in dependency order. // @name AI Repo Commander (Full Features) // @namespace http://tampermonkey.net/ // @version 2.1.0