diff --git a/Docs/diagrams/command-executor.puml b/Docs/diagrams/command-executor.puml new file mode 100644 index 0000000..288e9d6 --- /dev/null +++ b/Docs/diagrams/command-executor.puml @@ -0,0 +1,434 @@ +' =================================================================== +' File: command-executor.puml +' Purpose: Single source of truth for module-level activity + per-method sequences. +' Module: command-executor.js — Execute validated repo commands via bridge API; retries, timeouts, mock mode. +' Edit rules: Follow the legend at bottom; preserve VIEW/METHOD anchors for automation. +' =================================================================== + +' Neutral defaults — typography/layout only (keeps partition colors intact) +skinparam Shadowing false +skinparam SequenceMessageAlign center +skinparam SequenceLifeLineBorderColor #666666 +skinparam SequenceLifeLineBorderThickness 1 + +' ==== VIEW: Branch Flow (command-executor.js) ============================== +@startuml +title command-executor.js — Branch Flow (full module) + +start +:command-executor; + +fork + ' -------- execute(command, el, label) -------- + partition "execute(command, el, label)" #E7FAE3 { + :execute; + :log.command(action,'executing');\nautogen commit_message (for file ops); + :if api.disabled -> delay + _success(mock); + :else -> res=_api(command) -> _success(res); + :catch -> _error(err); + kill + } +fork again + ' -------- _api(command, attempt=0) -------- + partition "_api(command, attempt=0)" #FFF6D1 { + :_api; + :read cfg (maxRetries, timeout, bridgeKey);\nPOST JSON via GM_xmlhttpRequest; + :onload 2xx -> resolve;\nelse -> reject(Error status); + :onerror -> retry with backoff or fail; + :ontimeout -> reject(Error timeout); + kill + } +fork again + ' -------- _getBridgeKey() -------- + partition "_getBridgeKey()" #FFE1DB { + :_getBridgeKey; + :key = cfg.get('api.bridgeKey');\nif missing -> prompt;\noptional persist via cfg.set; + :return key or throw if empty; + kill + } +fork again + ' -------- _success(response, command, el, isMock, label) -------- + partition "_success(response, command, el, isMock, label)" #DCF9EE { + :_success; + :parse JSON responseText; _status(el, type);\nbranch to _handleGetFile/_handleListFiles; + :return { success:true, data, isMock }; + kill + } +fork again + ' -------- _error(error, command, el, label) -------- + partition "_error(error, command, el, label)" #FFE6F0 { + :_error; + :_status(el, 'ERROR', details);\nreturn { success:false, error }; + kill + } +fork again + ' -------- _status(el, type, data) -------- + partition "_status(el, type, data)" #E6F3FF { + :_status; + :create styled div;\nleft border color via _color(type);\nappend to el; + kill + } +fork again + ' -------- _handleGetFile(data, label) -------- + partition "_handleGetFile(data, label)" #F0E6FA { + :_handleGetFile; + :pull content from various shapes;\npush to window.AI_REPO_RESPONSES; + kill + } +fork again + ' -------- _handleListFiles(data, label) -------- + partition "_handleListFiles(data, label)" #E7FAF7 { + :_handleListFiles; + :files[] -> text fenced listing;\npush to window.AI_REPO_RESPONSES; + kill + } +fork again + ' -------- delay(ms) -------- + partition "delay(ms)" #FFF2E7 { + :delay; + :Promise(resolve after setTimeout(ms)); + kill + } +end fork +@enduml + +' ==== METHOD: execute(command, sourceElement, label) ======================== +@startuml +title command-executor:execute(command, el, label): \n Build request, call API or mock, normalize and render result + +participant "Caller" as CL +participant "execute()" as EXE +participant "delay(ms)" as DLY +participant "_api(command, attempt)" as API +participant "_success(...)" as OK +participant "_error(...)" as ERR +participant "Config" as CFG +participant "Logger" as LOG + +activate CL +CL -> EXE : initial request (command, el, label) +activate EXE + +EXE -> LOG : command(action,'executing', {repo,path,label}) +LOG --> EXE : ok + +' Auto-commit message for file ops +EXE -> EXE : if action in {update_file, create_file} and !commit_message\n commit_message = "AI Repo Commander: ... " + +' Mock path if API disabled +EXE -> CFG : get('api.enabled') +CFG --> EXE : true/false +alt api.enabled == false + EXE -> LOG : warn("API disabled, using mock") + EXE -> DLY : delay(300) + DLY --> EXE : done + EXE -> OK : _success({status:200,responseText:'{\"success\":true,...}'}, command, el, true, label) + OK --> EXE : result + EXE -> LOG : command(action,'complete', {mock:true}) + EXE --> CL : result +else real API call + EXE -> LOG : verbose("Making API request", {url,label}) + EXE -> API : _api(command) + API --> EXE : res {status, responseText} + EXE -> LOG : verbose("API request succeeded", {status}) + EXE -> OK : _success(res, command, el, false, label) + OK --> EXE : result + EXE -> LOG : command(action,'complete', {repo,path}) + EXE --> CL : result + + +' error handling +else error thrown + EXE -> LOG : command(action,'error', {error}) + EXE -> LOG : error("Command execution failed", {action,error}) + EXE -> ERR : _error(err, command, el, label) + ERR --> EXE : error result + EXE --> CL : error result +end + +deactivate EXE +deactivate CL +@enduml + +' ==== METHOD: _api(command, attempt=0) ===================================== +@startuml +title command-executor:_api(command, attempt=0): \n POST JSON via GM_xmlhttpRequest with retries and timeout + +participant "Caller" as CL +participant "_api(command, attempt)" as API +participant "_getBridgeKey()" as KEY +participant "Config" as CFG +participant "Logger" as LOG +participant "GM_xmlhttpRequest" as GMX +participant "Timer" as TMR + +activate CL +CL -> API : command, attempt=0 +activate API + +API -> CFG : get('api.maxRetries') / get('api.timeout') +CFG --> API : maxRetries / timeout +API -> KEY : _getBridgeKey() +KEY --> API : bridgeKey + +API -> LOG : trace("GM_xmlhttpRequest details", {method:'POST', url, timeout, hasKey, attempt}) +LOG --> API : ok + +API -> GMX : POST { url:command.url, headers:{X-Bridge-Key,Content-Type}, data:JSON.stringify(command), timeout } +alt onload 2xx + GMX --> API : response {status 2xx, responseText} + API --> CL : response +else onload error status + GMX --> API : response {status !2xx, statusText} + API --> CL : throws Error(`API Error ${status}: ${statusText}`) +else onerror (network) + GMX --> API : error + alt attempt < maxRetries + API -> LOG : warn("Network error, retrying", {nextDelay}) + API -> TMR : setTimeout(1000*(attempt+1)) + TMR --> API : wake + API -> API : return _api(command, attempt+1) + API --> CL : bubbled result + else max retries exceeded + API -> LOG : error("Network error, max retries exceeded") + API --> CL : throws Error(`Network error after ${attempt+1} attempts`) + end +else ontimeout + GMX --> API : timeout + API -> LOG : error("API request timed out", {timeout, action}) + API --> CL : throws Error(`API timeout after ${timeout}ms`) +end + +deactivate API +deactivate CL +@enduml + +' ==== METHOD: _getBridgeKey() ============================================== +@startuml +title command-executor:_getBridgeKey(): \n Read bridge key from config or prompt; optionally persist + +participant "Caller" as CL +participant "_getBridgeKey()" as KEY +participant "Config" as CFG +participant "Logger" as LOG +participant "Prompt" as PR +participant "Confirm" as CF + +activate CL +CL -> KEY : initial request +activate KEY + +KEY -> CFG : get('api.bridgeKey') +CFG --> KEY : key | undefined +alt key present + KEY -> LOG : trace("Using saved bridge key from config") + LOG --> KEY : ok + KEY --> CL : key +else key missing + KEY -> LOG : warn("Bridge key not found, prompting user") + LOG --> KEY : ok + KEY -> PR : prompt("Enter your bridge key…") + PR --> KEY : key | "" + alt empty string + KEY -> LOG : error("User did not provide bridge key") + KEY --> CL : throws Error('Bridge key required when API is enabled') + else provided + KEY -> CF : confirm("Save this bridge key?") + CF --> KEY : yes/no + alt yes + KEY -> CFG : set('api.bridgeKey', key) + CFG --> KEY : ok + KEY -> LOG : info("Bridge key saved to config") + else no + KEY -> LOG : info("Bridge key accepted for this session only") + end + KEY --> CL : key + end +end + +deactivate KEY +deactivate CL +@enduml + +' ==== METHOD: _success(response, command, el, isMock, label) ================ +@startuml +title command-executor:_success(response, command, el, isMock, label): \n Parse body, render status, route to output handlers + +participant "Caller" as CL +participant "_success(...)" as OK +participant "JSON" as JS +participant "_status(...)" as STAT +participant "_handleGetFile(...)" as HGF +participant "_handleListFiles(...)" as HLF + +activate CL +CL -> OK : response, command, el, isMock, label +activate OK + +OK -> JS : JSON.parse(response.responseText || "{}") +alt parse ok + JS --> OK : data +else parse error + JS --> OK : { message: "Operation completed" } +end + +OK -> STAT : _status(el, isMock?'MOCK':'SUCCESS', {action, details: data.message || 'Completed successfully', label}) +STAT --> OK : rendered + +alt command.action == "get_file" + OK -> HGF : _handleGetFile(data, label) + HGF --> OK : stored +else command.action == "list_files" + OK -> HLF : _handleListFiles(data, label) + HLF --> OK : stored +end + +OK --> CL : { success:true, data, isMock } +deactivate OK +deactivate CL +@enduml + +' ==== METHOD: _error(error, command, el, label) ============================= +@startuml +title command-executor:_error(error, command, el, label): \n Render error status and return failure object + +participant "Caller" as CL +participant "_error(...)" as ERR +participant "_status(...)" as STAT + +activate CL +CL -> ERR : error, command, el, label +activate ERR + +ERR -> STAT : _status(el, 'ERROR', {action: command.action, details: error.message, label}) +STAT --> ERR : rendered + +ERR --> CL : { success:false, error: error.message } +deactivate ERR +deactivate CL +@enduml + +' ==== METHOD: _status(el, type, data) ====================================== +@startuml +title command-executor:_status(el, type, data): \n Append styled status div to message element + +participant "Caller" as CL +participant "_status(...)" as STAT +participant "_color(type)" as CLR +participant "DOM" as DOM + +activate CL +CL -> STAT : el, type, data +activate STAT + +STAT -> CLR : _color(type) +CLR --> STAT : hex color +STAT -> DOM : createElement('div'); set styles (left border color) +DOM --> STAT : div +STAT -> DOM : set textContent `${label||action} — ${type}: details` +DOM --> STAT : ready +STAT -> DOM : appendChild(el, div) +DOM --> STAT : appended + +STAT --> CL : (void) +deactivate STAT +deactivate CL +@enduml + +' ==== METHOD: _handleGetFile(data, label) =================================== +@startuml +title command-executor:_handleGetFile(data, label): \n Extract content and store for paste-back + +participant "Caller" as CL +participant "_handleGetFile(...)" as HGF +participant "Logger" as LOG + +activate CL +CL -> HGF : data, label +activate HGF + +HGF -> HGF : content = data?.content?.data || data?.content || data?.result?.content?.data || data?.result?.content +alt content missing + HGF -> LOG : warn("get_file response missing content field") +else content present + HGF -> HGF : window.AI_REPO_RESPONSES ||= []; push({label, content}) + HGF -> LOG : verbose("File content stored for paste-back", {label, contentLength}) +end + +HGF --> CL : (void) +deactivate HGF +deactivate CL +@enduml + +' ==== METHOD: _handleListFiles(data, label) ================================= +@startuml +title command-executor:_handleListFiles(data, label): \n Build fenced listing of files and store for paste-back + +participant "Caller" as CL +participant "_handleListFiles(...)" as HLF +participant "Logger" as LOG + +activate CL +CL -> HLF : data, label +activate HLF + +HLF -> HLF : files = data?.files || data?.result?.files +alt files is Array + HLF -> HLF : listing = "```text\\n" + map(files)->path/name + "\\n```" + HLF -> HLF : window.AI_REPO_RESPONSES ||= []; push({label, content: listing}) + HLF -> LOG : verbose("File listing stored", {label, fileCount: files.length}) +else invalid files + HLF -> LOG : warn("list_files response missing files array") +end + +HLF --> CL : (void) +deactivate HLF +deactivate CL +@enduml + +' ==== METHOD: delay(ms) ===================================================== +@startuml +title command-executor:delay(ms): \n Resolve after a timeout + +participant "Caller" as CL +participant "delay(ms)" as DLY +participant "Timer" as TMR + +activate CL +CL -> DLY : ms +activate DLY +DLY -> TMR : setTimeout(ms) +TMR --> DLY : wake +DLY --> CL : (void) +deactivate DLY +deactivate CL +@enduml + +' ==== LEGEND =============================================================== +@startuml +legend bottom +== command-executor UML Style Guide (for future edits) == +• Scope: One .puml per module. Keep two views: + (1) Activity "Branch Flow" for the whole module (partitions + soft colors), + (2) Per-function Sequence diagrams for each exported or significant internal method. + +• Sequence conventions: + 1) First participant is the external caller (use "Caller" or a concrete origin). + 2) Do NOT add a module lifeline; the module name appears in the title only. + 3) Include every directly-called method or subsystem as a participant + (e.g., "execute()", "_api()", "_getBridgeKey()", "_success()", "_error()", "_status()", "_handleGetFile()", "_handleListFiles()", "GM_xmlhttpRequest", "JSON", "Config", "Logger", "Timer", "DOM", "Prompt", "Confirm"). + 4) Prefer simple messages; Use --> for returns; -> for calls. + 5) Use activate/deactivate as you see fit for clarity (no strict rule). + 6) Use alt blocks only when branches meaningfully change the message flow. + +• Activity view conventions: + A) Start with module node then fork partitions for each function/method. + B) One partition per function; soft background color; terminate branches with 'kill'. + C) Keep wording aligned with code (e.g., exact error text patterns, parameter names). + +• Color palette (soft pastels) +• Use --> for returns; -> for calls. +• Participants use quoted method names for internals (e.g., "execute()"), and plain nouns for systems ("JSON", "localStorage", "GM_xmlhttpRequest", "Prompt", "Confirm"). +• Keep this legend at the end of the file to standardize edits. +endlegend +@enduml diff --git a/Docs/diagrams/command-parser.puml b/Docs/diagrams/command-parser.puml new file mode 100644 index 0000000..e387ba3 --- /dev/null +++ b/Docs/diagrams/command-parser.puml @@ -0,0 +1,284 @@ +' =================================================================== +' File: command-parser.puml +' Purpose: Single source of truth for module-level activity + per-method sequences. +' Module: command-parser.js — Extract/parse YAML-like @bridge@ blocks; defaults; validation. +' Edit rules: Follow the legend at bottom; preserve VIEW/METHOD anchors for automation. +' =================================================================== + +' (Optional) neutral defaults — typography/layout only (keeps partition colors intact) +skinparam Shadowing false +skinparam SequenceMessageAlign center +skinparam SequenceLifeLineBorderColor #666666 +skinparam SequenceLifeLineBorderThickness 1 + +' ==== VIEW: Branch Flow (command-parser.js) ================================= +@startuml +title command-parser.js — Branch Flow (full module) + +start +:command-parser; + +fork + ' -------- REQUIRED (static map) -------- + partition "REQUIRED (action → required fields[])" #E7FAE3 { + :REQUIRED; + :get_file:[action,repo,path]\n\n update_file:[action,repo,path,content]\n\n create_file:[action,repo,path,content]\n\n create_repo:[action,repo]\n\n create_branch:[action,repo,branch]\n\n create_pr:[action,repo,title,head,base]\n\n list_files:[action,repo,path]; + kill + } +fork again + ' -------- parse(text) -------- + partition "parse(text)" #FFF6D1 { + :parse; + :block = extractBlock(text) or throw; + :obj = parseKV(block); + :applyDefaults(obj); + :return obj; + kill + } +fork again + ' -------- extractBlock(text) -------- + partition "extractBlock(text)" #FFE1DB { + :extractBlock; + :regex /^\s*@bridge@\\n([\\s\\S]*?)\\n@end@/m;\nreturn inner or null; + kill + } +fork again + ' -------- parseKV(block) -------- + partition "parseKV(block)" #DCF9EE { + :parseKV; + :scan lines; support "key: value" and "key: |" multiline; + :flush() helper to commit multiline; + :return object; + kill + } +fork again + ' -------- applyDefaults(obj) -------- + partition "applyDefaults(obj)" #FFE6F0 { + :applyDefaults; + :url default; owner default;\ncreate_branch -> source_branch default;\nrepo "owner/repo" split; + :return void; + kill + } +fork again + ' -------- validate(obj) -------- + partition "validate(obj)" #E6F3FF { + :validate; + :honor example flag -> {isValid:true, example:true}; + :check action presence + known action; + :ensure REQUIRED fields present; + :return {isValid, errors[], example?}; + kill + } +end fork +@enduml + +' ==== METHOD: parse(text) =================================================== +@startuml +title command-parser:parse(text): \n Extract first block, parse KV, apply defaults, return object + +participant "Caller" as CL +participant "parse(text)" as PAR +participant "extractBlock(text)" as EXT +participant "parseKV(block)" as PKV +participant "applyDefaults(obj)" as DEF + +activate CL +CL -> PAR : initial request (text) +activate PAR + +PAR -> EXT : extractBlock(text) +EXT --> PAR : block | null +alt block == null + PAR -> PAR : throw Error("No complete @bridge@ command found (missing @end@)") + PAR --> CL : (exception) ' (diagram note: thrown) +else block found + PAR -> PKV : parseKV(block) + PKV --> PAR : obj + PAR -> DEF : applyDefaults(obj) + DEF --> PAR : (void) + PAR --> CL : obj +end + +deactivate PAR +deactivate CL +@enduml + +' ==== METHOD: extractBlock(text) =========================================== +@startuml +title command-parser:extractBlock(text): \n Return inner text between @bridge@ and @end@ or null + +participant "Caller" as CL +participant "extractBlock(text)" as EXT + +activate CL +CL -> EXT : text +activate EXT +EXT -> EXT : m = /\\s*@bridge@\\s*\\n([\\s\\S]*?)\\n@end@/m.exec(text) +EXT -> EXT : return m?.[1]?.trim() || null +EXT --> CL : block | null +deactivate EXT +deactivate CL +@enduml + +' ==== METHOD: parseKV(block) =============================================== +@startuml +title command-parser:parseKV(block): \n Minimal YAML-like parser with multiline "|" support + +participant "Caller" as CL +participant "parseKV(block)" as PKV +participant "flush()" as FL + +activate CL +CL -> PKV : block +activate PKV + +PKV -> PKV : out = {}; lines = block.split('\\n')\ncurKey=null; multi=false; buf=[] +PKV -> FL : define flush(): if (multi && curKey) out[curKey]=buf.join('\\n').replace(/\\s+$/,''); reset +FL --> PKV : (ready) + +loop for each line + PKV -> PKV : normalize CR; line = raw.replace(/\\r$/,'') + alt multi is true + alt new unindented key pattern + PKV -> FL : flush() + FL --> PKV : ok + else still multiline + PKV -> PKV : buf.push(line) + ' continue + end + end + + PKV -> PKV : idx = line.indexOf(':') + alt idx != -1 + PKV -> PKV : key = line[0:idx].trim(); value = line[idx+1:].trim() + alt value == "|" + PKV -> PKV : curKey=key; multi=true; buf=[] + else single-line + PKV -> PKV : out[key] = value; curKey=key + end + else if (multi) + PKV -> PKV : buf.push(line) + end +end + +PKV -> FL : flush() +FL --> PKV : ok +PKV --> CL : out +deactivate PKV +deactivate CL +@enduml + +' ==== METHOD: applyDefaults(obj) =========================================== +@startuml +title command-parser:applyDefaults(obj): \n Set sane defaults and split owner/repo when needed + +participant "Caller" as CL +participant "applyDefaults(obj)" as DEF + +activate CL +CL -> DEF : obj +activate DEF + +DEF -> DEF : p.url ||= "https://n8n.brrd.tech/webhook/ai-gitea-bridge" +DEF -> DEF : p.owner ||= "rob" +DEF -> DEF : if action == "create_branch" && !source_branch -> "main" +DEF -> DEF : if typeof repo == string && repo.includes("/")\n [owner,repo] = repo.split("/",2);\n if !p.owner then p.owner=owner;\n p.repo=repo; + +DEF --> CL : (void) +deactivate DEF +deactivate CL +@enduml + +' ==== METHOD: validate(obj) ================================================ +@startuml +title command-parser:validate(obj): \n Respect example flag, ensure action known, check required fields + +participant "Caller" as CL +participant "validate(obj)" as VAL +participant "REQUIRED" as REQ +alt +else example flag + activate CL + CL -> VAL : obj + activate VAL + + ' example flag short-circuit + VAL -> VAL : if p.example == true/"true"/"yes" -> return {isValid:true, errors:[], example:true} + VAL --> CL : {isValid:true, errors:[], example:true} + deactivate VAL + ' (The diagram returns here in example case) +else no example flag + + ' Normal path (no example): + CL -> VAL : obj (no example) + activate VAL + + VAL -> VAL : if !p.action -> {isValid:false, errors:["Missing required field: action"]} + VAL -> REQ : req = REQUIRED[p.action] + REQ --> VAL : fields[] | undefined + alt + else unknown action + VAL --> CL : {isValid:false, errors:[`Unknown action: ${action}`]} + else known action + VAL -> VAL : for f in req: if missing -> errors.push(...) + VAL --> CL : {isValid: errors.length==0, errors} + end + + deactivate VAL + deactivate CL +end +@enduml + +' ==== LEGEND ================================================================ +@startuml +legend bottom +== command-parser UML Style Guide (for future edits) == +• Scope: One .puml per module. Keep two views: + (1) Activity "Branch Flow" for the whole module (partitions + soft colors), + (2) Per-function Sequence diagrams for each exported or significant internal method. + +• Sequence conventions: + 1) First participant is the external caller (use "Caller" or a concrete origin). + 2) Do NOT add a module lifeline; the module name appears in the title only. + 3) Include every directly-called method or subsystem as a participant + (e.g., "parse(text)", "extractBlock(text)", "parseKV(block)", "applyDefaults(obj)", "validate(obj)", "REQUIRED", "flush()"). + 4) Prefer simple messages; Use --> for returns; -> for calls. + 5) Use activate/deactivate as you see fit for clarity (no strict rule). + 6) Use alt blocks only when branches meaningfully change the message flow. + +• Activity view conventions: + A) Start with module node then fork partitions for each function/method. + B) One partition per function; soft background color; terminate branches with 'kill'. + C) Keep wording aligned with code (e.g., “deepClone(DEFAULT_CONFIG)”, exact error wording). + +• Color palette (soft pastels) +• Use --> for returns; -> for calls. +• Participants use quoted method names for internals (e.g., "parseKV(block)"), and plain nouns for structures ("REQUIRED"). +• Keep this legend at the end of the file to standardize edits. + +UML_Example +------------------------------------------ +title moduleName:methodName(args): \n Detailed description of what this method does + +participant "Caller" as CL +participant "methodName()" as M +' Add collaborators as needed: +' participant "Dependency" as DEP +' participant "AnotherMethod()" as AM + +activate CL +CL -> M : initial request (args) +activate M +' -- inner flow (keep alt blocks only if they clarify) -- +' M -> DEP : call something +' DEP --> M : result +' alt branch condition +' M -> AM : call another +' AM --> M : result +' end +M --> CL : return value +deactivate M +deactivate CL +------------------------------------------ +endlegend +@enduml diff --git a/Docs/diagrams/config.js.unified_views.puml b/Docs/diagrams/config.puml similarity index 98% rename from Docs/diagrams/config.js.unified_views.puml rename to Docs/diagrams/config.puml index 2c9cf16..1b1303c 100644 --- a/Docs/diagrams/config.js.unified_views.puml +++ b/Docs/diagrams/config.puml @@ -11,7 +11,7 @@ skinparam SequenceLifeLineBorderColor #666666 skinparam SequenceLifeLineBorderThickness 1 ' ==== VIEW: Branch Flow (full class) ========================================== -@startuml +@startuml ConfigManager title ConfigManager — Branch Flow (full class) start @@ -149,7 +149,7 @@ fork again @enduml ' ==== METHOD: constructor() ================================================ -@startuml +@startuml constructor title ConfigManager:constructor(): \n Populate this.config at instantiation actor Page as PG @@ -165,7 +165,7 @@ deactivate CTOR @enduml ' ==== METHOD: load() ======================================================= -@startuml +@startuml load title ConfigManager:load(): \n Read from localStorage, parse+merge or fallback to defaults participant "Caller" as CL @@ -207,7 +207,7 @@ end @enduml ' ==== METHOD: save() ======================================================= -@startuml +@startuml save title ConfigManager:save(): \n Strip runtime, stringify, persist to localStorage participant "Caller" as CL @@ -244,7 +244,7 @@ deactivate JS @enduml ' ==== METHOD: get(keyPath) ================================================ -@startuml +@startuml get title ConfigManager:get(keyPath): \n Resolve a dotted path or return undefined participant "Caller" as CL @@ -269,7 +269,7 @@ deactivate CL @enduml ' ==== METHOD: set(keyPath, value) ========================================= -@startuml +@startuml set title ConfigManager:set(keyPath, value): \n Create missing path segments, assign, then persist participant "Caller" as CL @@ -299,7 +299,7 @@ deactivate CL @enduml ' ==== METHOD: mergeConfigs(defaults, saved) ================================ -@startuml +@startuml mergeConfigs title ConfigManager:mergeConfigs(defaults, saved): \n Deep merge saved over defaults (skip runtime) participant "Caller" as CL @@ -337,7 +337,7 @@ deactivate CL @enduml ' ==== METHOD: deepClone(o) ================================================ -@startuml +@startuml deepClone title ConfigManager:deepClone(o): \n Structural clone for arrays/objects; primitives by value participant "Caller" as CL @@ -373,7 +373,7 @@ deactivate CL @enduml ' ==== LEGEND =============================================================== -@startuml +@startuml legend legend bottom == Config UML Style Guide (for future edits) == • Scope: One .puml per class or file. Keep two views: diff --git a/Docs/diagrams/debug-panel.puml b/Docs/diagrams/debug-panel.puml new file mode 100644 index 0000000..031a983 --- /dev/null +++ b/Docs/diagrams/debug-panel.puml @@ -0,0 +1,565 @@ +' =================================================================== +' File: debug-panel.puml +' Purpose: Single source of truth for module-level activity + per-method sequences. +' Module: debug-panel.js — Draggable in-page panel: logs tail, tools/settings, queue, pause/stop. +' Edit rules: Follow the legend at bottom; preserve VIEW/METHOD anchors for automation. +' =================================================================== + +' Neutral defaults — typography/layout only (keeps partition colors intact) +skinparam Shadowing false +skinparam SequenceMessageAlign center +skinparam SequenceLifeLineBorderColor #666666 +skinparam SequenceLifeLineBorderThickness 1 + +' ==== VIEW: Branch Flow (debug-panel.js) =================================== +@startuml +title debug-panel.js — Branch Flow (full module) + +start +:debug-panel; + +fork + ' -------- DebugPanel.constructor() -------- + partition "DebugPanel.constructor()" #E7FAE3 { + :constructor; + :panel/bodyLogs/bodyTools = null;\ncollapsed=false; drag={active:false,dx:0,dy:0}; + :panelState = _loadPanelState(); + kill + } +fork again + ' -------- _loadPanelState() -------- + partition "_loadPanelState()" #FFF6D1 { + :_loadPanelState; + :JSON.parse(localStorage.getItem(STORAGE_KEYS.panel)||'{}');\ncatch -> {}; + :return state; + kill + } +fork again + ' -------- _savePanelState(partial) -------- + partition "_savePanelState(partial)" #FFE1DB { + :_savePanelState; + :merged = {...panelState, ...partial};\npanelState=merged; localStorage.setItem(..., JSON.stringify(merged)); + kill + } +fork again + ' -------- flashBtn(btn, label, ms) -------- + partition "flashBtn(btn, label, ms)" #DCF9EE { + :flashBtn; + :guard !btn; disable; temp label ✓; fade; setTimeout restore; + kill + } +fork again + ' -------- toast(msg, ms) -------- + partition "toast(msg, ms)" #FFE6F0 { + :toast; + :guard !panel; create floating div; append; auto-remove; + kill + } +fork again + ' -------- copyLast(n=50) -------- + partition "copyLast(n=50)" #E6F3FF { + :copyLast; + :lines = logger.getRecentLogs(n);\ntry navigator.clipboard.writeText -> info + toast;\ncatch -> _fallbackCopy(lines); + kill + } +fork again + ' -------- _fallbackCopy(text, err?) -------- + partition "_fallbackCopy(text, err?)" #F0E6FA { + :_fallbackCopy; + :overlay+textarea UI; select+focus; Close button;\nwarn on missing Clipboard API; + kill + } +fork again + ' -------- mount() -------- + partition "mount()" #E7FAF7 { + :mount; + :guard document.body else defer;\nif !cfg.debug.showPanel return; + :build root DOM (header/tabs/controls/bodies);\nappend to body;\nset refs; _wireControls(); _startLogRefresh();\npost-mount log info; + kill + } +fork again + ' -------- _wireControls() -------- + partition "_wireControls()" #FFF2E7 { + :_wireControls; + :bind level select; copy buttons; pause/resume; queue clear;\nSTOP; tabs; collapse; dragging; clear history;\nfeature toggles; number inputs; bridge key;\nconfig JSON save/reset; + kill + } +fork again + ' -------- _loadToolsPanel() -------- + partition "_loadToolsPanel()" #E7F7FF { + :_loadToolsPanel; + :sync toggles/nums from cfg;\nmask bridge key; sanitize config JSON (mask bridgeKey); + kill + } +fork again + ' -------- _startLogRefresh() -------- + partition "_startLogRefresh()" #FFF7E7 { + :_startLogRefresh; + :renderLogs() each second: show tail of buffer or hints;\nautoscroll; + kill + } +fork again + ' -------- bootstrap -------- + partition "bootstrap (new + mount)" #F7E7FF { + :panel = new DebugPanel();\nif DOMContentLoaded pending -> add listener -> mount();\nelse mount();\nwindow.AI_REPO_DEBUG_PANEL = panel; + kill + } +end fork +@enduml + +' ==== METHOD: constructor() ================================================ +@startuml +title debug-panel:constructor(): \n Initialize DOM refs, drag state, and persisted panel state + +participant "Caller" as CL +participant "constructor()" as CTOR +participant "_loadPanelState()" as LPS + +activate CL +CL -> CTOR : new DebugPanel() +activate CTOR +CTOR -> CTOR : panel=null; bodyLogs=null; bodyTools=null; collapsed=false +CTOR -> CTOR : drag={active:false,dx:0,dy:0} +CTOR -> LPS : _loadPanelState() +LPS --> CTOR : state | {} +CTOR -> CTOR : panelState = state +CTOR --> CL : instance +deactivate CTOR +deactivate CL +@enduml + +' ==== METHOD: _loadPanelState() ============================================ +@startuml +title debug-panel:_loadPanelState(): \n Read panel position/collapsed state from localStorage + +participant "Caller" as CL +participant "_loadPanelState()" as LPS +participant "localStorage" as LS +participant "STORAGE_KEYS" as SK + +activate CL +CL -> LPS : initial request +activate LPS + +LPS -> SK : read STORAGE_KEYS.panel +SK --> LPS : key +LPS -> LS : getItem(key) +LS --> LPS : json | null +alt parse ok + LPS --> CL : JSON.parse(json) || {} +else parse error + LPS --> CL : {} +end + +deactivate LPS +deactivate CL +@enduml + +' ==== METHOD: _savePanelState(partial) ====================================== +@startuml +title debug-panel:_savePanelState(partial): \n Merge and persist panel state + +participant "Caller" as CL +participant "_savePanelState(partial)" as SPS +participant "localStorage" as LS +participant "STORAGE_KEYS" as SK + +activate CL +CL -> SPS : partial +activate SPS + +SPS -> SPS : merged = {...panelState, ...partial}; panelState=merged +SPS -> SK : STORAGE_KEYS.panel +SK --> SPS : key +SPS -> LS : setItem(key, JSON.stringify(merged)) +LS --> SPS : ok + +SPS --> CL : (void) +deactivate SPS +deactivate CL +@enduml + +' ==== METHOD: flashBtn(btn, label='Done', ms=900) =========================== +@startuml +title debug-panel:flashBtn(btn, label, ms): \n Temporarily disable and relabel a button + +participant "Caller" as CL +participant "flashBtn(btn,label,ms)" as FSH +participant "DOM" as DOM +participant "Timer" as TMR + +activate CL +CL -> FSH : btn, label, ms +activate FSH + +alt !btn + FSH --> CL : (void) + deactivate FSH + deactivate CL + return +end + +FSH -> DOM : btn.disabled=true; store old text; set `${label} ✓`; opacity=.7 +FSH -> TMR : setTimeout(ms) +TMR --> FSH : wake +FSH -> DOM : restore text; disabled=false; opacity='' +DOM --> FSH : ok +FSH --> CL : (void) +deactivate FSH +deactivate CL +@enduml + +' ==== METHOD: toast(msg, ms=1200) ========================================== +@startuml +title debug-panel:toast(msg, ms): \n Show a transient floating toast in panel + +participant "Caller" as CL +participant "toast(msg, ms)" as TST +participant "DOM" as DOM +participant "Timer" as TMR + +activate CL +CL -> TST : msg, ms +activate TST + +alt !panel + TST --> CL : (void) + deactivate TST + deactivate CL + return +end + +TST -> DOM : createElement('div') with styles; appendChild(panel) +DOM --> TST : elem +TST -> TMR : setTimeout(ms) +TMR --> TST : wake +TST -> DOM : elem.remove() +DOM --> TST : removed + +TST --> CL : (void) +deactivate TST +deactivate CL +@enduml + +' ==== METHOD: copyLast(n=50) =============================================== +@startuml +title debug-panel:copyLast(n): \n Copy recent logs to clipboard or show manual copy overlay + +participant "Caller" as CL +participant "copyLast(n)" as CPL +participant "Logger" as LOG +participant "Navigator.clipboard" as NCB +participant "toast(msg)" as TST +participant "_fallbackCopy(text,err?)" as FBC + +activate CL +CL -> CPL : n +activate CPL + +CPL -> LOG : getRecentLogs(n) +LOG --> CPL : lines (string) + +CPL -> NCB : writeText(lines) +alt clipboard ok + NCB --> CPL : resolved + CPL -> LOG : info("Copied last n lines") + CPL -> TST : toast(`Copied last ${n} logs`) + TST --> CPL : shown +else clipboard error or unsupported + NCB --> CPL : rejected | (no API) + CPL -> FBC : _fallbackCopy(lines, error?) + FBC --> CPL : overlay shown +end + +CPL --> CL : (void) +deactivate CPL +deactivate CL +@enduml + +' ==== METHOD: _fallbackCopy(text, originalError) ============================ +@startuml +title debug-panel:_fallbackCopy(text, originalError): \n Fullscreen overlay with textarea for manual copy + +participant "Caller" as CL +participant "_fallbackCopy(text,err?)" as FBC +participant "DOM" as DOM +participant "Logger" as LOG + +activate CL +CL -> FBC : text, originalError? +activate FBC + +FBC -> DOM : create overlay + panel + textarea + Close button; append to body +DOM --> FBC : mounted +FBC -> DOM : textarea.focus(); textarea.select() +DOM --> FBC : selected +FBC -> LOG : warn("Clipboard API unavailable", {error}) +LOG --> FBC : ok + +FBC --> CL : (void) +deactivate FBC +deactivate CL +@enduml + +' ==== METHOD: mount() ======================================================= +@startuml +title debug-panel:mount(): \n Create panel DOM, wire controls, start log refresh, log ready + +participant "Caller" as CL +participant "mount()" as MNT +participant "Config" as CFG +participant "DOM" as DOM +participant "_wireControls()" as WRC +participant "_startLogRefresh()" as LRF +participant "Logger" as LOG +participant "Timer" as TMR + +activate CL +CL -> MNT : initial request +activate MNT + +alt !document.body + MNT -> TMR : setTimeout(100) + TMR --> MNT : retry + MNT --> CL : (returns after scheduling) +else body present + MNT -> CFG : get('debug.showPanel') + CFG --> MNT : bool + alt disabled + MNT --> CL : (void) + else enabled + MNT -> DOM : build root HTML (header/tabs/controls/bodies) + DOM --> MNT : root + MNT -> DOM : appendChild(document.body, root) + DOM --> MNT : ok + MNT -> MNT : this.panel/root refs; bodyLogs/bodyTools + MNT -> WRC : _wireControls() + WRC --> MNT : wired + MNT -> LRF : _startLogRefresh() + LRF --> MNT : started + MNT -> TMR : setTimeout(100) + TMR --> MNT : tick + MNT -> LOG : info("Debug panel mounted...") + LOG --> MNT : ok + MNT -> LOG : info("Panel visible at: (...)") + end + MNT --> CL : (void) +end + +deactivate MNT +deactivate CL +@enduml + +' ==== METHOD: _wireControls() ============================================== +@startuml +title debug-panel:_wireControls(): \n Bind all UI controls to config/logger/queue/history/actions + +participant "Caller" as CL +participant "_wireControls()" as WRC +participant "Config" as CFG +participant "Logger" as LOG +participant "DOM" as DOM +participant "Queue" as QUE +participant "History" as HIS +participant "Paste" as PST + +activate CL +CL -> WRC : initial request +activate WRC + +' Log level selector +WRC -> DOM : select('.rc-level') +DOM --> WRC : sel +WRC -> CFG : get('debug.level') +CFG --> WRC : currentLevel +WRC -> LOG : trace(`[Debug Panel] Current log level: ${currentLevel}`) +WRC -> DOM : sel.onchange -> LOG.setLevel(newLevel) + LOG.info(...) + +' Copy buttons +WRC -> DOM : .rc-copy / .rc-copy-200 (onclick -> copyLast(n) + flashBtn) +DOM --> WRC : wired + +' Pause/Resume runtime +WRC -> DOM : .rc-pause (onclick -> toggle cfg.runtime.paused; flashBtn; toast; log) +DOM --> WRC : wired + +' Queue clear +WRC -> DOM : .rc-queue-clear (onclick -> AI_REPO_QUEUE.clear(); flashBtn; toast; log.warn) +DOM --> WRC : wired + +' Emergency STOP +WRC -> DOM : .rc-stop (onclick -> AI_REPO_STOP(); flashBtn; toast; log.warn) +DOM --> WRC : wired + +' Tabs + Tools load +WRC -> DOM : tabs ('Logs'/'Tools & Settings'); switch bodies; call _loadToolsPanel() +DOM --> WRC : wired + +' Collapse toggle +WRC -> DOM : .rc-collapse (onclick -> toggle collapsed; save state) +DOM --> WRC : wired + +' Drag header +WRC -> DOM : .rc-header (mousedown -> track; mousemove -> set left/top; mouseup -> _savePanelState) +DOM --> WRC : wired + +' Clear History +WRC -> DOM : .rc-clear-history (onclick -> HIS.clear(); GM_notification?; flashBtn; toast; log) +DOM --> WRC : wired + +' Toggles (checkboxes) +WRC -> DOM : .rc-toggle (bind cfg.get/set; toast; log.info) +DOM --> WRC : wired + +' Numeric inputs +WRC -> DOM : .rc-num (bind cfg.get; on change -> parseInt -> cfg.set; toast; log.info) +DOM --> WRC : wired + +' Bridge key save/clear +WRC -> DOM : .rc-save-bridge-key / .rc-clear-bridge-key (onclick -> cfg.set('api.bridgeKey', val|''); mask input; flashBtn; toast; log) +DOM --> WRC : wired + +' Config JSON save/reset +WRC -> DOM : .rc-save-json (parse JSON; delete meta/runtime; deep-set cfg; refresh tools; toast/log), .rc-reset-defaults (confirm -> localStorage.removeItem(cfgKey) -> reload) +DOM --> WRC : wired + +WRC --> CL : (void) +deactivate WRC +deactivate CL +@enduml + +' ==== METHOD: _loadToolsPanel() ============================================ +@startuml +title debug-panel:_loadToolsPanel(): \n Populate controls from cfg and render sanitized JSON + +participant "Caller" as CL +participant "_loadToolsPanel()" as LTP +participant "Config" as CFG +participant "DOM" as DOM + +activate CL +CL -> LTP : initial request +activate LTP + +' Toggles and nums +LTP -> DOM : queryAll('.rc-toggle') / queryAll('.rc-num') +DOM --> LTP : lists +LTP -> CFG : get(key) for each +CFG --> LTP : values +LTP -> DOM : set .checked / .value accordingly + +' Bridge key masked +LTP -> CFG : get('api.bridgeKey') +CFG --> LTP : key | '' +LTP -> DOM : .rc-bridge-key.value = key ? '••••••••' : '' + +' Config JSON sanitized +LTP -> CFG : config (object) +CFG --> LTP : dump +LTP -> LTP : sanitized = deep clone; if api.bridgeKey -> mask +LTP -> DOM : .rc-json.value = JSON.stringify(sanitized, null, 2) + +LTP --> CL : (void) +deactivate LTP +deactivate CL +@enduml + +' ==== METHOD: _startLogRefresh() =========================================== +@startuml +title debug-panel:_startLogRefresh(): \n Tail logger buffer every second and autoscroll + +participant "Caller" as CL +participant "_startLogRefresh()" as LRF +participant "Logger" as LOG +participant "DOM" as DOM +participant "Timer" as TMR + +activate CL +CL -> LRF : initial request +activate LRF + +LRF -> LRF : define renderLogs() +LRF -> TMR : setInterval(renderLogs, 1000) +TMR --> LRF : id +LRF -> LRF : renderLogs() + +' renderLogs inner flow +LRF -> LOG : buffer? +LOG --> LRF : buffer | undefined +alt logger not ready + LRF -> DOM : bodyLogs.innerHTML = "Logger not initialized yet..." +else ready + LRF -> LRF : rows = buffer.slice(-80) + alt rows empty + LRF -> DOM : "No logs yet. Waiting for activity..." + else rows + LRF -> DOM : bodyLogs.innerHTML = rows.map(...).join('') + LRF -> DOM : bodyLogs.scrollTop = bodyLogs.scrollHeight + end +end + +LRF --> CL : (void) +deactivate LRF +deactivate CL +@enduml + +' ==== METHOD: bootstrap (instance + mount) ================================= +@startuml +title debug-panel:bootstrap: \n Instantiate panel and mount after DOM is ready + +participant "Caller" as CL +participant "bootstrap" as BOOT +participant "DOM" as DOM +participant "DebugPanel()" as CTOR +participant "mount()" as MNT + +activate CL +CL -> BOOT : initial load +activate BOOT + +BOOT -> CTOR : new DebugPanel() +CTOR --> BOOT : panel +BOOT -> DOM : document.readyState +DOM --> BOOT : 'loading' | 'interactive/complete' +alt loading + BOOT -> DOM : addEventListener('DOMContentLoaded', () => panel.mount()) + DOM --> BOOT : listener added +else ready + BOOT -> MNT : panel.mount() + MNT --> BOOT : mounted +end + +BOOT -> DOM : window.AI_REPO_DEBUG_PANEL = panel +BOOT --> CL : (void) +deactivate BOOT +deactivate CL +@enduml + +' ==== LEGEND =============================================================== +@startuml +legend bottom +== debug-panel UML Style Guide (for future edits) == +• Scope: One .puml per module. Keep two views: + (1) Activity "Branch Flow" for the whole module (partitions + soft colors), + (2) Per-function/Per-method Sequence diagrams for each exported or significant internal function. + +• Sequence conventions: + 1) First participant is the external caller (use "Caller" or a concrete origin). + 2) Do NOT add a module/class lifeline; the name appears in the title only. + 3) Include every directly-called method or subsystem as a participant + (e.g., "mount()", "_wireControls()", "copyLast()", "_fallbackCopy()", "toast()", "flashBtn()", "Queue", "History", "Logger", "Config", "Timer", "DOM", "localStorage", "STORAGE_KEYS"). + 4) Prefer simple messages; Use --> for returns; -> for calls. + 5) Use activate/deactivate as you see fit for clarity. + 6) Use alt blocks only when branches meaningfully change the message flow. + +• Activity view conventions: + A) Start with module node then fork partitions for each function/method. + B) One partition per function; soft background color; terminate branches with 'kill'. + C) Keep wording aligned with code (e.g., masked bridge key, JSON sanitize, tail size ~80 rows). + +• Color palette (soft pastels) +• Use --> for returns; -> for calls. +• Participants use quoted method names for internals (e.g., "_loadToolsPanel()"), and plain nouns for systems ("DOM", "Timer", "localStorage"). +• Keep this legend at the end of the file to standardize edits. +endlegend +@enduml diff --git a/Docs/diagrams/detector.puml b/Docs/diagrams/detector.puml new file mode 100644 index 0000000..e270c37 --- /dev/null +++ b/Docs/diagrams/detector.puml @@ -0,0 +1,473 @@ +' =================================================================== +' File: detector.puml +' Purpose: Single source of truth for module-level activity + per-method sequences. +' Module: detector.js — Observe assistant messages, settle text, extract @bridge@ blocks, enqueue. +' Edit rules: Follow the legend at bottom; preserve VIEW/METHOD anchors for automation. +' =================================================================== + +' ==== VIEW: Branch Flow (detector.js) ======================================= +@startuml + +skinparam Shadowing false +skinparam SequenceMessageAlign center +skinparam SequenceLifeLineBorderColor #666666 +skinparam SequenceLifeLineBorderThickness 1 + +title detector.js — Branch Flow (full module) + +start +:detector; + +fork + ' -------- extractAllBlocks(text) -------- + partition "extractAllBlocks(text)" #E7FAE3 { + :extractAllBlocks; + :regex /^\s*@bridge@...@end@/gm; + :return array of blocks; + kill + } +fork again + ' -------- isAssistantMsg(el) -------- + partition "isAssistantMsg(el)" #FFF6D1 { + :isAssistantMsg; + :match known selectors / descendants; + :return true/false; + kill + } +fork again + ' -------- settleText(el, initial, windowMs, pollMs) -------- + partition "settleText(el, initial, windowMs, pollMs)" #FFE1DB { + :settleText; + :deadline = now + windowMs; + :poll text every pollMs;\nconcat blocks; reset deadline on change; + :return last stable pick; + kill + } +fork again + ' -------- Detector.constructor() -------- + partition "Detector.constructor()" #DCF9EE { + :constructor; + :observer=null; processed=WeakSet;\nclusterLookahead=3; clusterWindowMs=1000; + kill + } +fork again + ' -------- Detector.start() -------- + partition "Detector.start()" #FFE6F0 { + :start; + :guard: if observer exists -> warn+return; + :create MutationObserver (childList, characterData, attributes); + :attach to document.body (subtree); + :optionally process existing messages (cfg.ui.processExisting); + kill + } +fork again + ' -------- Detector._handle(el) -------- + partition "Detector._handle(el)" #E6F3FF { + :_handle; + :skip if already processed;\nadd to processed; + :debounce (cfg.execution.debounceDelay); + :stable = settleText(...); + :blocks = extractAllBlocks(stable); + :if blocks empty -> unmark + return; + :cap by cfg.queue.maxPerMessage;\nfor each: _enqueueOne(el, text, idx); + :schedule _clusterRescan(el) after window; + kill + } +fork again + ' -------- Detector._enqueueOne(el, text, idx) -------- + partition "Detector._enqueueOne(el, text, idx)" #F0E6FA { + :_enqueueOne; + :if history.isProcessed -> add run-again button + return; + :history.markProcessed; + :AI_REPO_QUEUE.push(async task { parse -> validate -> execute | catch -> run-again }); + kill + } +fork again + ' -------- Detector._addRunAgain(el, text, idx) -------- + partition "Detector._addRunAgain(el, text, idx)" #E7FAF7 { + :_addRunAgain; + :create button "Run Again #n"; onClick -> _enqueueOne(...); + :append to message el; + kill + } +fork again + ' -------- Detector._clusterRescan(anchor) -------- + partition "Detector._clusterRescan(anchor)" #FFF2E7 { + :_clusterRescan; + :walk nextElementSibling up to clusterLookahead; + :if assistant & not processed -> _handle(cur); + kill + } +end fork +@enduml + +' ==== METHOD: extractAllBlocks(text) ======================================== +@startuml +title detector:extractAllBlocks(text): \n Return all complete @bridge@...@end@ fenced blocks + +participant "Caller" as CL +participant "extractAllBlocks(text)" as EXT + +activate CL +CL -> EXT : initial request (text) +activate EXT +EXT -> EXT : regex /^\s*@bridge@\\n([\\s\\S]*?)\\n@end@/gm +EXT --> CL : blocks[] +deactivate EXT +deactivate CL +@enduml + +' ==== METHOD: isAssistantMsg(el) ============================================ +@startuml +title detector:isAssistantMsg(el): \n Identify assistant/authored content nodes in the DOM + +participant "Caller" as CL +participant "isAssistantMsg(el)" as IAM + +activate CL +CL -> IAM : initial request (el) +activate IAM +IAM -> IAM : check selectors\n[data-message-author-role="assistant"]\n.chat-message:not([data-message-author-role="user"])\n.message-content +IAM --> CL : true/false +deactivate IAM +deactivate CL +@enduml + +' ==== METHOD: settleText(el, initial, windowMs, pollMs) ===================== +@startuml +title detector:settleText(el, initial, windowMs, pollMs): \n Wait for streaming text to stabilize + +participant "Caller" as CL +participant "settleText(...)" as ST +participant "Timer" as TMR +participant "extractAllBlocks(text)" as EXT + +activate CL +CL -> ST : initial request (el, initial, windowMs, pollMs) +activate ST + +ST -> ST : deadline = now + windowMs; last = initial +loop poll until deadline + ST -> TMR : setTimeout(pollMs) + TMR --> ST : wake + ST -> ST : fresh = el.textContent || '' + ST -> EXT : extractAllBlocks(fresh) + EXT --> ST : blocks[] + ST -> ST : pick = blocks.join('\\n') + alt pick == last AND pick not empty + ST -> ST : continue (keep waiting within window) + else pick changed AND not empty + ST -> ST : last = pick; deadline = now + windowMs + end +end + +ST --> CL : last +deactivate ST +deactivate CL +@enduml + +' ==== METHOD: Detector.constructor() ======================================== +@startuml +title detector:constructor(): \n Initialize observer, processed set, cluster knobs + +participant "Caller" as CL +participant "constructor()" as CTOR + +activate CL +CL -> CTOR : new Detector() +activate CTOR +CTOR -> CTOR : observer=null; processed=WeakSet()\nclusterLookahead=3; clusterWindowMs=1000 +CTOR --> CL : instance +deactivate CTOR +deactivate CL +@enduml + +' ==== METHOD: Detector.start() ============================================== +@startuml +title detector:start(): \n Attach MutationObserver; optionally process existing messages + +participant "Caller" as CL +participant "start()" as ST +participant "Logger" as LOG +participant "Config" as CFG +participant "MutationObserver" as MO +participant "DOM" as DOM + +activate CL +CL -> ST : initial request +activate ST + +ST -> ST : if observer exists -> LOG.warn + return +ST -> LOG : info("Starting advanced detector…") +LOG --> ST : ok + +ST -> MO : new MutationObserver(callback) +MO --> ST : observer +ST -> DOM : observe(document.body, { childList, subtree, characterData, attributes }) +DOM --> ST : attached + +ST -> CFG : get('ui.processExisting') +CFG --> ST : true/false +alt process existing + ST -> DOM : querySelectorAll('[data-message-author-role], .chat-message, .message-content') + DOM --> ST : NodeList + ST -> ST : for each el: if isAssistantMsg(el) -> _handle(el) +else skip + ST -> LOG : trace("processExisting disabled") +end + +ST -> LOG : info("Detector started and monitoring") +ST --> CL : started +deactivate ST +deactivate CL +@enduml + +' ==== METHOD: Detector._handle(el) ========================================== +@startuml +title detector:_handle(el): \n Debounce, settle text, extract blocks, enqueue, schedule cluster rescan + +participant "Caller" as CL +participant "_handle(el)" as HDL +participant "Logger" as LOG +participant "Config" as CFG +participant "settleText(...)" as ST +participant "extractAllBlocks(text)" as EXT +participant "_enqueueOne(el, text, idx)" as ENQ +participant "_clusterRescan(anchor)" as CRS +participant "Timer" as TMR + +activate CL +CL -> HDL : initial request (el) +activate HDL + +HDL -> HDL : if processed.has(el) -> LOG.trace + return +HDL -> HDL : processed.add(el) + +HDL -> LOG : verbose("Processing new assistant message") +LOG --> HDL : ok + +HDL -> CFG : get('execution.debounceDelay') +CFG --> HDL : debounce (ms) +alt debounce > 0 + HDL -> LOG : trace(`Debouncing for ${debounce}ms`) + HDL -> TMR : setTimeout(debounce) + TMR --> HDL : wake +end + +HDL -> HDL : baseText = el.textContent || '' +HDL -> LOG : trace("Starting text settle check", { textLength }) +HDL -> CFG : get('execution.settleCheckMs') / get('execution.settlePollMs') +CFG --> HDL : settleMs / pollMs +HDL -> ST : settleText(el, baseText, settleMs, pollMs) +ST --> HDL : stable + +HDL -> EXT : extractAllBlocks(stable) +EXT --> HDL : blocks[] + +HDL -> LOG : verbose(`Found ${blocks.length} block(s)`) +alt blocks.length == 0 + HDL -> LOG : trace("No blocks; removing from processed") + HDL -> HDL : processed.delete(el) + HDL --> CL : done + deactivate HDL + deactivate CL + return +end + +HDL -> CFG : get('queue.maxPerMessage') +CFG --> HDL : maxPerMsg +alt blocks.length > maxPerMsg + HDL -> LOG : warn(`limiting to ${maxPerMsg}`) +end + +HDL -> HDL : for (idx, cmdText) in blocks.slice(0, maxPerMsg) +HDL -> ENQ : _enqueueOne(el, cmdText, idx) +ENQ --> HDL : queued + +HDL -> LOG : trace(`Scheduling cluster rescan`) +HDL -> TMR : setTimeout(clusterWindowMs) +TMR --> HDL : wake +HDL -> CRS : _clusterRescan(el) +CRS --> HDL : done + +HDL --> CL : done +deactivate HDL +deactivate CL +@enduml + +' ==== METHOD: Detector._enqueueOne(el, commandText, idx) ==================== +@startuml +title detector:_enqueueOne(el, commandText, idx): \n History gate; queue async task (parse → validate → execute) + +participant "Caller" as CL +participant "_enqueueOne(...)" as ENQ +participant "Logger" as LOG +participant "History" as HST +participant "Queue" as QUE +participant "Parser" as PAR +participant "Executor" as EXE +participant "_addRunAgain(el, text, idx)" as RAG + +activate CL +CL -> ENQ : initial request (el, text, idx) +activate ENQ + +ENQ -> HST : isProcessed(el, idx) +HST --> ENQ : true/false +alt already processed + ENQ -> LOG : verbose("already processed, adding retry button") + ENQ -> RAG : _addRunAgain(el, text, idx) + RAG --> ENQ : button added + ENQ --> CL : done + deactivate ENQ + deactivate CL + return +end + +ENQ -> LOG : trace("mark processed") +ENQ -> HST : markProcessed(el, idx) +HST --> ENQ : ok + +ENQ -> LOG : verbose("pushing command to queue") +ENQ -> QUE : push(async task) +QUE --> ENQ : ok + +' --- async task (conceptual) --- +ENQ -> PAR : parse(text) +PAR --> ENQ : parsed (action, repo, path, ...) +ENQ -> PAR : validate(parsed) +PAR --> ENQ : { isValid, errors?, example? } + +alt example is true + ENQ -> LOG : info("Example command skipped") +else invalid + ENQ -> LOG : warn("Validation failed", errors) + ENQ -> RAG : _addRunAgain(el, text, idx) + RAG --> ENQ : added +else valid + ENQ -> LOG : verbose(`Executing: ${parsed.action}`) + ENQ -> EXE : execute(parsed, el, `[${idx+1}] ${action}`) + EXE --> ENQ : result | error +end + +ENQ --> CL : done +deactivate ENQ +deactivate CL +@enduml + +' ==== METHOD: Detector._addRunAgain(el, commandText, idx) =================== +@startuml +title detector:_addRunAgain(el, commandText, idx): \n Add a “Run Again #n” button that re-enqueues the command + +participant "Caller" as CL +participant "_addRunAgain(...)" as RAG +participant "DOM" as DOM +participant "_enqueueOne(...)" as ENQ + +activate CL +CL -> RAG : initial request (el, text, idx) +activate RAG + +RAG -> DOM : createElement('button') +DOM --> RAG : btn +RAG -> DOM : set text, style, click handler -> ENQ(...) +DOM --> RAG : configured +RAG -> DOM : append btn to el +DOM --> RAG : appended + +RAG --> CL : done +deactivate RAG +deactivate CL +@enduml + +' ==== METHOD: Detector._clusterRescan(anchor) =============================== +@startuml +title detector:_clusterRescan(anchor): \n Scan next siblings for assistant messages and handle them + +participant "Caller" as CL +participant "_clusterRescan(anchor)" as CRS +participant "DOM" as DOM +participant "isAssistantMsg(el)" as IAM +participant "_handle(el)" as HDL + +activate CL +CL -> CRS : initial request (anchor) +activate CRS + +CRS -> DOM : cur = anchor.nextElementSibling +loop up to clusterLookahead + CRS -> IAM : isAssistantMsg(cur) + IAM --> CRS : true/false + alt is assistant + CRS -> CRS : if not processed -> _handle(cur) + CRS -> HDL : _handle(cur) + HDL --> CRS : done + else not assistant + CRS -> CRS : break + end + CRS -> DOM : cur = cur.nextElementSibling +end + +CRS --> CL : done +deactivate CRS +deactivate CL +@enduml + +' ==== LEGEND ================================================================ +@startuml +legend bottom +== detector UML Style Guide (for future edits) == +• Scope: One .puml per module. Keep two views: + (1) Activity "Branch Flow" for the whole module (partitions + soft colors), + (2) Per-function Sequence diagrams for each exported or significant internal method. + +• Sequence conventions: + 1) First participant is the external caller (use "Caller" or a concrete origin like "Page"). + 2) Do NOT add a module lifeline; the module name appears in the title only. + 3) Include every directly-called method or subsystem as a participant + (e.g., "extractAllBlocks()", "isAssistantMsg()", "settleText()", "Parser", "Executor", "Queue", "History", "Config", "Logger", "Timer", "DOM"). + 4) Prefer simple messages; Use --> for returns; -> for calls. + 5) Use activate/deactivate as you see fit for clarity (no strict rule). + 6) Use alt blocks only when branches meaningfully change the message flow. + +• Activity view conventions: + A) Start with module node then fork partitions for each function/method. + B) One partition per function; soft background color; terminate branches with 'kill'. + C) Keep wording aligned with code (e.g., "deepClone(DEFAULT_CONFIG)", "mergeConfigs(...)"). + +• Color palette (soft pastels) +• Keep this legend at the end of the file to standardize edits. + +UML_Example +------------------------------------------ +title moduleName:methodName(args): \n Detailed description of what this method does + +participant "Caller" as CL +participant "methodName()" as M +' Add collaborators as needed: +' participant "Dependency" as DEP +' participant "AnotherMethod()" as AM +' participant "DOM" as DOM +' participant "Timer" as TMR + +activate CL +CL -> M : initial request (args) +activate M +' -- inner flow (keep alt blocks only if they clarify) -- +' M -> DEP : call something +' DEP --> M : result + +' alt branch condition +' M -> AM : call another +' AM --> M : result +' else other branch +' M -> DOM : do something with the DOM +' DOM --> M : ok +' end + +M --> CL : return value +deactivate M +deactivate CL +------------------------------------------ +endlegend +@enduml diff --git a/Docs/diagrams/fingerprint-strong.puml b/Docs/diagrams/fingerprint-strong.puml new file mode 100644 index 0000000..9f1f9e2 --- /dev/null +++ b/Docs/diagrams/fingerprint-strong.puml @@ -0,0 +1,439 @@ +' =================================================================== +' File: fingerprint-strong.puml +' Purpose: Single source of truth for module-level activity + per-function sequences. +' Module: fingerprint-strong.js — Stable fingerprints for assistant message elements. +' Edit rules: Follow the legend at bottom; preserve VIEW/METHOD anchors for automation. +' =================================================================== + +' Neutral defaults — typography/layout only (keeps partition colors intact) +skinparam Shadowing false +skinparam SequenceMessageAlign center +skinparam SequenceLifeLineBorderColor #666666 +skinparam SequenceLifeLineBorderThickness 1 + +' ==== VIEW: Branch Flow (fingerprint-strong.js) ============================= +@startuml +title fingerprint-strong.js — Branch Flow (full module) + +start +:fingerprint-strong; + +fork + ' -------- MSG_SELECTORS (const) -------- + partition "MSG_SELECTORS (assistant message selectors)" #E7FAE3 { + :[ '[data-message-author-role=\"assistant\"]',\n '.chat-message:not([data-message-author-role=\"user\"])',\n '.message-content' ]; + :Used by context scans and ordinal computation; + kill + } +fork again + ' -------- norm(s) -------- + partition "norm(s)" #FFF6D1 { + :norm; + :remove \\r and zero-width spaces;\ncollapse spaces before \\n; trim; + :return normalized string; + kill + } +fork again + ' -------- hash(s) -------- + partition "hash(s)" #FFE1DB { + :hash; + :djb2-xor over chars;\nreturn unsigned base36; + kill + } +fork again + ' -------- commandLikeText(el) -------- + partition "commandLikeText(el)" #DCF9EE { + :commandLikeText; + :prefer
 blocks that contain full @bridge@...@end@ with action:;\nelse fallback to el.textContent (<=2000 chars), normalized;
+    :return text slice;
+    kill
+  }
+fork again
+  ' -------- prevContextHash(el) --------
+  partition "prevContextHash(el)" #FFE6F0 {
+    :prevContextHash;
+    :scan previous assistant messages via MSG_SELECTORS;\ncollect trailing text up to 2000 chars; hash();
+    :return base36;
+    kill
+  }
+fork again
+  ' -------- intraPrefixHash(el) --------
+  partition "intraPrefixHash(el)" #E6F3FF {
+    :intraPrefixHash;
+    :find first @bridge@ block; hash the text just before it (<=2000 chars);\nif none, hash trailing slice of whole text;
+    :return base36;
+    kill
+  }
+fork again
+  ' -------- domHint(node) --------
+  partition "domHint(node)" #F0E6FA {
+    :domHint;
+    :build tag#id.class (first class only), slice 40 chars;
+    :return small hint;
+    kill
+  }
+fork again
+  ' -------- ordinalForKey(el, key) --------
+  partition "ordinalForKey(el, key)" #E7FAF7 {
+    :ordinalForKey;
+    :scan all message nodes; compute same-key for peers;\nreturn index among matches;
+    kill
+  }
+fork again
+  ' -------- fingerprintElement(el) --------
+  partition "fingerprintElement(el)" #FFF2E7 {
+    :fingerprintElement;
+    :ch = hash(commandLikeText(el));\nph = prevContextHash(el);\nih = intraPrefixHash(el);\nkey=`ch:..|ph:..|ih:..`;\nn = ordinalForKey(el,key);\ndh = hash(domHint(el));\nreturn key+`|hint:${dh}|n:${n}`;
+    kill
+  }
+fork again
+  ' -------- getStableFingerprint(el) --------
+  partition "getStableFingerprint(el)" #E7F7FF {
+    :getStableFingerprint;
+    :if dataset.aiRcStableFp present -> return;\nelse compute fingerprintElement(el) and cache in dataset;
+    :return fp;
+    kill
+  }
+fork again
+  ' -------- expose globals --------
+  partition "bootstrap (expose globals)" #FFF7E7 {
+    :window.AI_REPO_FINGERPRINT = fingerprintElement;\nwindow.AI_REPO_STABLE_FINGERPRINT = getStableFingerprint;
+    kill
+  }
+end fork
+@enduml
+
+' ==== METHOD: norm(s) =======================================================
+@startuml
+title fingerprint-strong:norm(s): \n Normalize text for stable hashing
+
+participant "Caller" as CL
+participant "norm(s)" as NRM
+
+activate CL
+CL -> NRM : s
+activate NRM
+NRM -> NRM : s = (s||'')\n  .replace(/\\r/g,'')\n  .replace(/\\u200b/g,'')\n  .replace(/[ \\t]+\\n/g,'\\n')\n  .trim()
+NRM --> CL : normalized s
+deactivate NRM
+deactivate CL
+@enduml
+
+' ==== METHOD: hash(s) =======================================================
+@startuml
+title fingerprint-strong:hash(s): \n djb2-xor -> unsigned base36
+
+participant "Caller" as CL
+participant "hash(s)" as HSH
+
+activate CL
+CL -> HSH : s
+activate HSH
+HSH -> HSH : h=5381; for each char: h=((h<<5)+h)^code
+HSH --> CL : (h>>>0).toString(36)
+deactivate HSH
+deactivate CL
+@enduml
+
+' ==== METHOD: commandLikeText(el) ==========================================
+@startuml
+title fingerprint-strong:commandLikeText(el): \n Prefer fenced @bridge@ blocks; fallback to raw text
+
+participant "Caller" as CL
+participant "commandLikeText(el)" as CLT
+participant "norm(s)" as NRM
+participant "DOM" as DOM
+
+activate CL
+CL -> CLT : el
+activate CLT
+
+CLT -> DOM : el.querySelectorAll('pre code, pre, code')
+DOM --> CLT : NodeList blocks
+
+loop for each block b
+  CLT -> NRM : norm(b.textContent||'')
+  NRM --> CLT : t
+  alt looks like full @bridge@ ... @end@ and contains 'action:'
+    CLT --> CL : t
+    deactivate CLT
+    deactivate CL
+    return
+  end
+end
+
+' fallback to element text
+CLT -> NRM : norm((el.textContent||'').slice(0,2000))
+NRM --> CLT : text
+CLT --> CL : text
+deactivate CLT
+deactivate CL
+@enduml
+
+' ==== METHOD: prevContextHash(el) ==========================================
+@startuml
+title fingerprint-strong:prevContextHash(el): \n Hash trailing text of prior assistant messages (≤2000 chars)
+
+participant "Caller" as CL
+participant "prevContextHash(el)" as PCH
+participant "DOM" as DOM
+participant "norm(s)" as NRM
+participant "hash(s)" as HSH
+
+activate CL
+CL -> PCH : el
+activate PCH
+
+PCH -> DOM : querySelectorAll(MSG_SELECTORS.join(','))
+DOM --> PCH : list[]
+PCH -> PCH : idx = list.indexOf(el)
+alt idx <= 0
+  PCH --> CL : "0"
+  deactivate PCH
+  deactivate CL
+  return
+end
+
+PCH -> PCH : rem=2000; buf=''
+loop for i = idx-1 down to 0 while rem>0
+  PCH -> NRM : norm(list[i].textContent||'')
+  NRM --> PCH : t
+  alt t non-empty
+    PCH -> PCH : take = t.slice(-rem); buf = take + buf; rem -= take.length
+  end
+end
+
+PCH -> HSH : hash(buf.slice(-2000))
+HSH --> PCH : ph
+PCH --> CL : ph
+deactivate PCH
+deactivate CL
+@enduml
+
+' ==== METHOD: intraPrefixHash(el) ==========================================
+@startuml
+title fingerprint-strong:intraPrefixHash(el): \n Hash text immediately before first command block
+
+participant "Caller" as CL
+participant "intraPrefixHash(el)" as IPH
+participant "hash(s)" as HSH
+participant "norm(s)" as NRM
+
+activate CL
+CL -> IPH : el
+activate IPH
+
+IPH -> IPH : t = el.textContent||''
+IPH -> IPH : m = t.match(/@bridge@[\\s\\S]*?@end@/m)
+IPH -> IPH : endIdx = m ? t.indexOf(m[0]) : t.length
+IPH -> NRM : norm(t.slice(max(0,endIdx-2000), endIdx))
+NRM --> IPH : slice
+IPH -> HSH : hash(slice)
+HSH --> IPH : ih
+IPH --> CL : ih
+deactivate IPH
+deactivate CL
+@enduml
+
+' ==== METHOD: domHint(node) ================================================
+@startuml
+title fingerprint-strong:domHint(node): \n Tiny tag#id.class hint (first class, ≤40 chars)
+
+participant "Caller" as CL
+participant "domHint(node)" as DH
+participant "DOM" as DOM
+
+activate CL
+CL -> DH : node
+activate DH
+alt !node
+  DH --> CL : ""
+  deactivate DH
+  deactivate CL
+  return
+end
+
+DH -> DOM : node.id / node.className / node.tagName
+DOM --> DH : values
+DH -> DH : cls = (typeof className==='string' ? className.split(' ')[0] : '')\n hint = `${tag}#${id}.${cls}`.slice(0,40)
+DH --> CL : hint
+deactivate DH
+deactivate CL
+@enduml
+
+' ==== METHOD: ordinalForKey(el, key) =======================================
+@startuml
+title fingerprint-strong:ordinalForKey(el, key): \n Index el among elements with same ch|ph|ih
+
+participant "Caller" as CL
+participant "ordinalForKey(el, key)" as ORD
+participant "DOM" as DOM
+participant "commandLikeText(el)" as CLT
+participant "prevContextHash(el)" as PCH
+participant "intraPrefixHash(el)" as IPH
+participant "hash(s)" as HSH
+
+activate CL
+CL -> ORD : el, key
+activate ORD
+
+ORD -> DOM : querySelectorAll(MSG_SELECTORS.join(','))
+DOM --> ORD : list[]
+ORD -> ORD : n = 0
+loop for each node in list
+  alt node === el
+    ORD -> ORD : nodeKey = key
+  else other node
+    ORD -> CLT : commandLikeText(node)
+    CLT --> ORD : text
+    ORD -> HSH : hash(text.slice(0,2000))
+    HSH --> ORD : ch
+    ORD -> PCH : prevContextHash(node)
+    PCH --> ORD : ph
+    ORD -> IPH : intraPrefixHash(node)
+    IPH --> ORD : ih
+    ORD -> ORD : nodeKey = `ch:${ch}|ph:${ph}|ih:${ih}`
+  end
+
+  alt nodeKey == key
+    alt node === el
+      ORD --> CL : n
+      deactivate ORD
+      deactivate CL
+      return
+    else
+      ORD -> ORD : n++
+    end
+  end
+end
+
+ORD --> CL : n
+deactivate ORD
+deactivate CL
+@enduml
+
+' ==== METHOD: fingerprintElement(el) =======================================
+@startuml
+title fingerprint-strong:fingerprintElement(el): \n Compose ch|ph|ih + hint + ordinal
+
+participant "Caller" as CL
+participant "fingerprintElement(el)" as FPE
+participant "commandLikeText(el)" as CLT
+participant "prevContextHash(el)" as PCH
+participant "intraPrefixHash(el)" as IPH
+participant "domHint(node)" as DH
+participant "ordinalForKey(el, key)" as ORD
+participant "hash(s)" as HSH
+
+activate CL
+CL -> FPE : el
+activate FPE
+
+' ch
+FPE -> CLT : commandLikeText(el)
+CLT --> FPE : text
+FPE -> HSH : hash(text.slice(0,2000))
+HSH --> FPE : ch
+
+' ph / ih
+FPE -> PCH : prevContextHash(el)
+PCH --> FPE : ph
+FPE -> IPH : intraPrefixHash(el)
+IPH --> FPE : ih
+
+FPE -> FPE : key = `ch:${ch}|ph:${ph}|ih:${ih}`
+
+' ordinal
+FPE -> ORD : ordinalForKey(el, key)
+ORD --> FPE : n
+
+' hint
+FPE -> DH : domHint(el)
+DH --> FPE : hint
+FPE -> HSH : hash(hint)
+HSH --> FPE : dh
+
+FPE --> CL : `ch:${ch}|ph:${ph}|ih:${ih}|hint:${dh}|n:${n}`
+deactivate FPE
+deactivate CL
+@enduml
+
+' ==== METHOD: getStableFingerprint(el) =====================================
+@startuml
+title fingerprint-strong:getStableFingerprint(el): \n Cache-first wrapper over fingerprintElement(el)
+
+participant "Caller" as CL
+participant "getStableFingerprint(el)" as GSF
+participant "fingerprintElement(el)" as FPE
+participant "Dataset" as DATA
+
+activate CL
+CL -> GSF : el
+activate GSF
+
+GSF -> DATA : read el.dataset.aiRcStableFp
+DATA --> GSF : fp | undefined
+alt cached
+  GSF --> CL : fp
+else not cached
+  GSF -> FPE : fingerprintElement(el)
+  FPE --> GSF : fp
+  GSF -> DATA : el.dataset.aiRcStableFp = fp (try/catch)
+  DATA --> GSF : ok | ignored
+  GSF --> CL : fp
+end
+
+deactivate GSF
+deactivate CL
+@enduml
+
+' ==== METHOD: bootstrap (expose globals) ===================================
+@startuml
+title fingerprint-strong:bootstrap: \n Expose helpers on window for consumers
+
+participant "Caller" as CL
+participant "bootstrap" as BOOT
+participant "fingerprintElement(el)" as FPE
+participant "getStableFingerprint(el)" as GSF
+participant "window" as WIN
+
+activate CL
+CL -> BOOT : module load
+activate BOOT
+BOOT -> WIN : WIN.AI_REPO_FINGERPRINT = FPE
+WIN --> BOOT : ok
+BOOT -> WIN : WIN.AI_REPO_STABLE_FINGERPRINT = GSF
+WIN --> BOOT : ok
+BOOT --> CL : (void)
+deactivate BOOT
+deactivate CL
+@enduml
+
+' ==== LEGEND ===============================================================
+@startuml
+legend bottom
+== fingerprint-strong UML Style Guide (for future edits) ==
+• Scope: One .puml per module. Keep two views:
+  (1) Activity "Branch Flow" for the whole module (partitions + soft colors),
+  (2) Per-function Sequence diagrams for each exported or significant helper.
+
+• Sequence conventions:
+  1) First participant is the external caller (use "Caller" or a concrete origin).
+  2) Do NOT add a module lifeline; the module name appears in the title only.
+  3) Include every directly-called helper/system as a participant
+     (e.g., "commandLikeText()", "prevContextHash()", "intraPrefixHash()", "domHint()", "ordinalForKey()", "hash()", "norm()", "Dataset", "DOM", "window").
+  4) Prefer simple messages; Use --> for returns; -> for calls.
+  5) Use activate/deactivate as you see fit for clarity.
+  6) Use alt blocks only when branches meaningfully change the message flow.
+
+• Activity view conventions:
+  A) Start with module node then fork partitions for each function.
+  B) One partition per function; soft background color; terminate branches with 'kill'.
+  C) Keep wording aligned with code (e.g., 2000-char slices, base36 hash, dataset cache key).
+
+• Color palette (soft pastels)
+• Use --> for returns; -> for calls.
+• Participants use quoted function names for internals; plain nouns for systems ("DOM", "Dataset", "window").
+• Keep this legend at the end of the file to standardize edits.
+endlegend
+@enduml
diff --git a/Docs/diagrams/logger.puml b/Docs/diagrams/logger.puml
new file mode 100644
index 0000000..727522d
--- /dev/null
+++ b/Docs/diagrams/logger.puml
@@ -0,0 +1,438 @@
+' ===================================================================
+' File: logger.puml
+' Purpose: Single source of truth for module-level activity + per-method sequences.
+' Module: logger.js — Structured logger with level gating, buffer, anti-spam utilities.
+' Edit rules: Follow the legend at bottom; preserve VIEW/METHOD anchors for automation.
+' ===================================================================
+
+' Neutral defaults — typography/layout only (keeps partition colors intact)
+skinparam Shadowing false
+skinparam SequenceMessageAlign center
+skinparam SequenceLifeLineBorderColor #666666
+skinparam SequenceLifeLineBorderThickness 1
+
+' ==== VIEW: Branch Flow (logger.js) ========================================
+@startuml
+title logger.js — Branch Flow (full module)
+
+start
+:logger;
+
+fork
+  ' -------- Logger.constructor() --------
+  partition "Logger.constructor()" #E7FAE3 {
+    :constructor;
+    :config = window.AI_REPO_CONFIG;\nbuffer=[]; loopCounts=Map(); startedAt=Date.now();
+    :setInterval(clean loopCounts if > 2× watchMs);
+    kill
+  }
+fork again
+  ' -------- error/warn/info/verbose/trace --------
+  partition "level helpers (error/warn/info/verbose/trace)" #FFF6D1 {
+    :error(msg,data) -> _log(1,'ERROR',...);
+    :warn(msg,data)  -> _log(2,'WARN',...);
+    :info(msg,data)  -> _log(3,'INFO',...);
+    :verbose(msg,data)-> _log(4,'VERBOSE',...);
+    :trace(msg,data) -> _log(5,'TRACE',...);
+    kill
+  }
+fork again
+  ' -------- command(action,status,extra) --------
+  partition "command(action, status, extra)" #FFE1DB {
+    :command;
+    :icon by status; info(`${icon} ${action} [${status}]`, extra);
+    kill
+  }
+fork again
+  ' -------- logLoop(kind, msg) --------
+  partition "logLoop(kind, msg)" #DCF9EE {
+    :logLoop;
+    :k=kind+msg; cur=loopCounts.get(k)||0;
+    :withinWatch = now-startedAt <= watchMs;
+    :if !withinWatch && kind!='WARN' -> return;
+    :if cur>=10 -> return;
+    :loopCounts.set(k,cur+1); suffix=(cur+1>1?` (${cur+1}x)`:'');
+    :route to error/warn/info(kind, msg+suffix);
+    kill
+  }
+fork again
+  ' -------- _log(levelNum, levelName, msg, data) --------
+  partition "_log(levelNum, levelName, msg, data)" #FFE6F0 {
+    :_log;
+    :enabled = !!config.debug.enabled;\nlevel = config.debug.level ?? 0;\nif !enabled or levelNum>level -> return;
+    :entry={ts, levelName, message, data:_sanitize(data)};
+    :buffer.push(entry); trim to debug.maxLines (default 400);
+    :console.log(`[AI RC ${levelName}]`, msg, optional sanitized data);
+    kill
+  }
+fork again
+  ' -------- _sanitize(data) --------
+  partition "_sanitize(data)" #E6F3FF {
+    :_sanitize;
+    :null -> null;\nHTMLElement -> "HTMLElement";
+    :long string (>200) -> truncated + ellipsis;
+    :plain object -> shallow sanitize values recursively (HTMLElement/long string);
+    :else return data;
+    kill
+  }
+fork again
+  ' -------- getRecentLogs(n=50) --------
+  partition "getRecentLogs(n=50)" #F0E6FA {
+    :getRecentLogs;
+    :tail buffer n; map to lines: ISO LEVEL message {jsonData?}; join with \\n;
+    kill
+  }
+fork again
+  ' -------- setLevel(n) --------
+  partition "setLevel(n)" #E7FAF7 {
+    :setLevel;
+    :lv = clamp(n,0..5); config.set('debug.level', lv);\ninfo(`Log level => ${lv}`);
+    kill
+  }
+fork again
+  ' -------- bootstrap (global) --------
+  partition "bootstrap (global)" #FFF7E7 {
+    :window.AI_REPO_LOGGER = new Logger();
+    kill
+  }
+end fork
+@enduml
+
+
+' ==== METHOD: Logger.constructor() =========================================
+@startuml
+title logger:constructor(): \n Prepare config, buffers, anti-spam counters, and cleanup timer
+
+participant "Caller" as CL
+participant "constructor()" as CTOR
+participant "Config" as CFG
+participant "Timer" as TMR
+
+activate CL
+CL -> CTOR : new Logger()
+activate CTOR
+
+CTOR -> CFG : window.AI_REPO_CONFIG
+CFG --> CTOR : config
+CTOR -> CTOR : buffer=[]; loopCounts=new Map(); startedAt=Date.now()
+
+' periodic cleanup ~ every watchMs (default 120s)
+CTOR -> CFG : get('debug.watchMs') || 120000
+CFG --> CTOR : watchMs
+CTOR -> TMR : setInterval(fn, watchMs)
+TMR --> CTOR : id
+CTOR -> CTOR : fn: if (Date.now()-startedAt > watchMs*2)\n  loopCounts.clear(); startedAt=Date.now()
+
+CTOR --> CL : instance
+deactivate CTOR
+deactivate CL
+@enduml
+
+
+' ==== METHOD: level helpers (error/warn/info/verbose/trace) ================
+@startuml
+title logger:level helpers: \n Route to _log() with appropriate numeric level
+
+participant "Caller" as CL
+participant "error()" as ERR
+participant "warn()" as WRN
+participant "info()" as INF
+participant "verbose()" as VRB
+participant "trace()" as TRC
+participant "_log(...)" as LOG
+
+activate CL
+CL -> ERR : msg, data
+ERR -> LOG : _log(1,'ERROR',msg,data)
+LOG --> ERR : (void)
+
+CL -> WRN : msg, data
+WRN -> LOG : _log(2,'WARN',msg,data)
+LOG --> WRN : (void)
+
+CL -> INF : msg, data
+INF -> LOG : _log(3,'INFO',msg,data)
+LOG --> INF : (void)
+
+CL -> VRB : msg, data
+VRB -> LOG : _log(4,'VERBOSE',msg,data)
+LOG --> VRB : (void)
+
+CL -> TRC : msg, data
+TRC -> LOG : _log(5,'TRACE',msg,data)
+LOG --> TRC : (void)
+
+TRC --> CL : done
+deactivate TRC
+deactivate CL
+@enduml
+
+
+' ==== METHOD: command(action, status, extra) ================================
+@startuml
+title logger:command(action, status, extra): \n Friendly lifecycle log with icon
+
+participant "Caller" as CL
+participant "command(action,status,extra)" as CMD
+participant "info()" as INF
+
+activate CL
+CL -> CMD : action, status, extra
+activate CMD
+CMD -> CMD : icon = map(status)\n('👁️','📝','✓','⏳','⚙️','✅','❌','•')
+CMD -> INF : info(`${icon} ${action} [${status}]`, extra)
+INF --> CMD : (void)
+CMD --> CL : (void)
+deactivate CMD
+deactivate CL
+@enduml
+
+
+' ==== METHOD: logLoop(kind, msg) ===========================================
+@startuml
+title logger:logLoop(kind, msg): \n Anti-spam logging for hot paths (max 10 per watch window)
+
+participant "Caller" as CL
+participant "logLoop(kind,msg)" as LLP
+participant "Config" as CFG
+participant "error()" as ERR
+participant "warn()" as WRN
+participant "info()" as INF
+
+activate CL
+CL -> LLP : kind, msg
+activate LLP
+
+LLP -> LLP : k=`${kind}:${msg}`; cur=loopCounts.get(k)||0
+LLP -> CFG : get('debug.watchMs') || 120000
+CFG --> LLP : watchMs
+LLP -> LLP : withinWatch = (now-startedAt) <= watchMs
+alt !withinWatch && kind != 'WARN'
+  LLP --> CL : (void)
+  deactivate LLP
+  deactivate CL
+  return
+end
+alt cur >= 10
+  LLP --> CL : (void)
+  deactivate LLP
+  deactivate CL
+  return
+end
+
+LLP -> LLP : loopCounts.set(k, cur+1);\nsuffix = (cur+1>1) ? ` (${cur+1}x)` : ''
+alt kind == 'ERROR'
+  LLP -> ERR : error(msg+suffix)
+  ERR --> LLP : ok
+else kind == 'WARN'
+  LLP -> WRN : warn(msg+suffix)
+  WRN --> LLP : ok
+else
+  LLP -> INF : info(msg+suffix)
+  INF --> LLP : ok
+end
+
+LLP --> CL : (void)
+deactivate LLP
+deactivate CL
+@enduml
+
+
+' ==== METHOD: _log(levelNum, levelName, msg, data) =========================
+@startuml
+title logger:_log(levelNum, levelName, msg, data): \n Gate by config, buffer tail, print to console
+
+participant "Caller" as CL
+participant "_log(...)" as LOG
+participant "Config" as CFG
+participant "_sanitize(data)" as SAN
+participant "Console" as CON
+
+activate CL
+CL -> LOG : levelNum, levelName, msg, data
+activate LOG
+
+LOG -> CFG : get('debug.enabled')
+CFG --> LOG : enabled (bool)
+LOG -> CFG : get('debug.level') ?? 0
+CFG --> LOG : level (0..5)
+
+alt !enabled or levelNum > level
+  LOG --> CL : (void)
+  deactivate LOG
+  deactivate CL
+  return
+end
+
+LOG -> SAN : _sanitize(data)
+SAN --> LOG : safeData
+
+LOG -> LOG : entry = {timestamp, level:levelName, message:String(msg), data:safeData}
+LOG -> CFG : get('debug.maxLines') || 400
+CFG --> LOG : maxLines
+LOG -> LOG : buffer.push(entry); if buffer.length>maxLines -> splice head
+
+' console output
+alt with data
+  LOG -> CON : console.log(`[AI RC ${levelName}]`, msg, entry.data)
+else no data
+  LOG -> CON : console.log(`[AI RC ${levelName}]`, msg)
+end
+CON --> LOG : printed
+
+LOG --> CL : (void)
+deactivate LOG
+deactivate CL
+@enduml
+
+
+' ==== METHOD: _sanitize(data) ==============================================
+@startuml
+title logger:_sanitize(data): \n Redact DOM elements, truncate long strings, shallow-sanitize objects
+
+participant "Caller" as CL
+participant "_sanitize(data)" as SAN
+participant "HTMLElement" as HTM
+
+activate CL
+CL -> SAN : data
+activate SAN
+
+alt !data
+  SAN --> CL : null
+  deactivate SAN
+  deactivate CL
+  return
+end
+
+alt data instanceof HTMLElement
+  SAN -> HTM : tagName
+  HTM --> SAN : TAG
+  SAN --> CL : "HTMLElement"
+  deactivate SAN
+  deactivate CL
+  return
+end
+
+alt typeof data === 'string' && length > 200
+  SAN --> CL : data.slice(0,200) + '…'
+  deactivate SAN
+  deactivate CL
+  return
+end
+
+alt typeof data === 'object'
+  SAN -> SAN : out = {}
+  loop for each [k,v] of Object.entries(data)
+    alt v instanceof HTMLElement
+      SAN -> SAN : out[k] = `HTMLElement<${v.tagName}>`
+    else typeof v === 'string' && v.length>200
+      SAN -> SAN : out[k] = v.slice(0,200) + '…'
+    else
+      SAN -> SAN : out[k] = v
+    end
+  end
+  SAN --> CL : out
+else other primitive
+  SAN --> CL : data
+end
+
+deactivate SAN
+deactivate CL
+@enduml
+
+
+' ==== METHOD: getRecentLogs(n=50) ==========================================
+@startuml
+title logger:getRecentLogs(n): \n Tail buffer to plain-text lines for copy/export
+
+participant "Caller" as CL
+participant "getRecentLogs(n)" as GRL
+
+activate CL
+CL -> GRL : n (default 50)
+activate GRL
+
+GRL -> GRL : tail = buffer.slice(-n)
+GRL -> GRL : lines = tail.map(e => `${e.timestamp} ${e.level.padEnd(7)} ${e.message}${e.data ? ' ' + JSON.stringify(e.data) : ''}`)
+GRL --> CL : lines.join('\\n')
+deactivate GRL
+deactivate CL
+@enduml
+
+
+' ==== METHOD: setLevel(n) ==================================================
+@startuml
+title logger:setLevel(n): \n Clamp and persist numeric level, then self-log the change
+
+participant "Caller" as CL
+participant "setLevel(n)" as SLV
+participant "Config" as CFG
+participant "info()" as INF
+
+activate CL
+CL -> SLV : n
+activate SLV
+
+SLV -> SLV : lv = Math.max(0, Math.min(5, n))
+SLV -> CFG : set('debug.level', lv)
+CFG --> SLV : ok
+SLV -> INF : info(`Log level => ${lv}`)
+INF --> SLV : logged
+
+SLV --> CL : (void)
+deactivate SLV
+deactivate CL
+@enduml
+
+
+' ==== METHOD: bootstrap (global) ===========================================
+@startuml
+title logger:bootstrap: \n Expose singleton logger on window
+
+participant "Caller" as CL
+participant "bootstrap" as BOOT
+participant "Logger()" as CTOR
+participant "window" as WIN
+
+activate CL
+CL -> BOOT : module load
+activate BOOT
+BOOT -> CTOR : new Logger()
+CTOR --> BOOT : instance
+BOOT -> WIN : window.AI_REPO_LOGGER = instance
+WIN --> BOOT : ok
+BOOT --> CL : (void)
+deactivate BOOT
+deactivate CL
+@enduml
+
+
+' ==== LEGEND ===============================================================
+@startuml
+legend bottom
+== logger UML Style Guide (for future edits) ==
+• Scope: One .puml per module. Keep two views:
+  (1) Activity "Branch Flow" for the whole module (partitions + soft colors),
+  (2) Per-function/Per-method Sequence diagrams for each exported or significant internal method.
+
+• Sequence conventions:
+  1) First participant is the external caller (use "Caller" or a concrete origin).
+  2) Do NOT add a module/class lifeline; the name appears in the title only.
+  3) Include every directly-called helper/system as a participant
+     (e.g., "info()", "_log()", "_sanitize()", "Config", "Console", "Timer", "HTMLElement").
+  4) Prefer simple messages; Use --> for returns; -> for calls.
+  5) Use activate/deactivate as you see fit for clarity.
+  6) Use alt blocks only when branches meaningfully change the message flow.
+
+• Activity view conventions:
+  A) Start with module node then fork partitions for each function/method.
+  B) One partition per function; soft background color; terminate branches with 'kill'.
+  C) Keep wording aligned with code (e.g., watchMs default 120000, maxLines default 400, 10x cap in logLoop).
+
+• Color palette (soft pastels)
+• Use --> for returns; -> for calls.
+• Participants use quoted method names for internals; plain nouns for systems ("Config", "Console", "Timer").
+• Keep this legend at the end of the file to standardize edits.
+endlegend
+@enduml
diff --git a/Docs/diagrams/main.puml b/Docs/diagrams/main.puml
new file mode 100644
index 0000000..7a98781
--- /dev/null
+++ b/Docs/diagrams/main.puml
@@ -0,0 +1,605 @@
+' ===================================================================
+' File: main.puml
+' Purpose: Single source of truth for module-level activity + per-method sequences.
+' Module: main.js — Legacy entry + convenience API; wires observer and exposes helpers.
+' Edit rules: Follow the legend at bottom; preserve VIEW/METHOD anchors for automation.
+' ===================================================================
+
+' Neutral defaults — typography/layout only (keeps partition colors intact)
+skinparam Shadowing false
+skinparam SequenceMessageAlign center
+skinparam SequenceLifeLineBorderColor #666666
+skinparam SequenceLifeLineBorderThickness 1
+
+' ==== VIEW: Branch Flow (main.js) ==========================================
+@startuml
+title main.js — Branch Flow (full module)
+
+start
+:main;
+
+fork
+  ' -------- AIRepoCommander.constructor() --------
+  partition "AIRepoCommander.constructor()" #E7FAE3 {
+    :constructor;
+    :isInitialized=false; observer=null;\nprocessed=WeakSet();\nmessageSelectors=[assistant selectors];
+    kill
+  }
+fork again
+  ' -------- initialize() --------
+  partition "initialize()" #FFF6D1 {
+    :initialize;
+    :guard: if already initialized -> warn + return;\nlog meta+debug+api;\nlog config summary;
+    :startObserver(); if cfg.ui.processExisting -> scanExisting(); exposeAPI();\nset isInitialized=true; log initialized; trace globals;
+    kill
+  }
+fork again
+  ' -------- startObserver() --------
+  partition "startObserver()" #FFE1DB {
+    :startObserver;
+    :create MutationObserver(cb);\ncb skips when runtime.paused;\ncount assistant messages (direct + nested); call processMessage(el);\nobserve document.body {childList,subtree};\nlog started;
+    kill
+  }
+fork again
+  ' -------- isAssistantMessage(el) --------
+  partition "isAssistantMessage(el)" #DCF9EE {
+    :isAssistantMessage;
+    :return any selector matches(el);
+    kill
+  }
+fork again
+  ' -------- processMessage(el) --------
+  partition "processMessage(el)" #FFE6F0 {
+    :processMessage;
+    :skip if processed.has(el);\ncommands=extractCommands(el);\nif none -> trace & return;\nmark processed; cap to cfg.queue.maxPerMessage;
+    :for each cmd i: if history.isProcessed(el,i) -> addRetryButton(); else run(el,cmd,i);
+    kill
+  }
+fork again
+  ' -------- extractCommands(el) --------
+  partition "extractCommands(el)" #E6F3FF {
+    :extractCommands;
+    :regex /@bridge@[\\s\\S]*?@end@/g over el.textContent;\nreturn blocks[];
+    kill
+  }
+fork again
+  ' -------- run(el, commandText, index) --------
+  partition "run(el, commandText, index)" #F0E6FA {
+    :run;
+    :trace + markProcessed(el,index);\nparsed = PARSER.parse(commandText);\nvalidation = PARSER.validate(parsed);
+    :if !validation.isValid -> error + addRetryButton();\nif example -> info + return;\noptional debounce via cfg.execution.debounceDelay;
+    :label = `Command ${i+1}`; EXECUTOR.execute(parsed, el, label);\nlog success; catch -> error + addRetryButton();
+    kill
+  }
+fork again
+  ' -------- addRetryButton(el, txt, idx) --------
+  partition "addRetryButton(el, txt, idx)" #E7FAF7 {
+    :addRetryButton;
+    :create