added comments

This commit is contained in:
rob 2025-10-17 18:32:11 -03:00
parent c408289e0f
commit 3fcfb6b6e0
13 changed files with 4676 additions and 9 deletions

View File

@ -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: ... <ISO>"
' 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

View File

@ -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

View File

@ -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:

View File

@ -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

473
Docs/diagrams/detector.puml Normal file
View File

@ -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

View File

@ -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 <pre><code> 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

438
Docs/diagrams/logger.puml Normal file
View File

@ -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<TAG>";
: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<TAG>"
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

605
Docs/diagrams/main.puml Normal file
View File

@ -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 <button> Run Again #idx;\nwire click -> run(el, txt, idx);\nappend to el;
kill
}
fork again
' -------- scanExisting() --------
partition "scanExisting()" #FFF2E7 {
:scanExisting;
:nodes = querySelectorAll(messageSelectors);\nlog count; for each assistant node -> processMessage(el);\nlog scanned summary;
kill
}
fork again
' -------- exposeAPI() --------
partition "exposeAPI()" #E7F7FF {
:exposeAPI;
:window.AI_REPO = {version,config,logger,history, pause(), resume(), clearHistory()};\nwindow.AI_REPO_STOP() => disable API + pause + clear queue + error logs;\nwindow.AI_REPO_SET_KEY(k) -> cfg.set('api.bridgeKey',k);
kill
}
fork again
' -------- delay(ms) --------
partition "delay(ms)" #FFF7E7 {
:delay;
:return Promise(setTimeout(ms));
kill
}
fork again
' -------- destroy() --------
partition "destroy()" #F7E7FF {
:destroy;
:observer?.disconnect(); processed=WeakSet(); isInitialized=false; log destroyed;
kill
}
fork again
' -------- bootstrap (DOMContentLoaded / ready) --------
partition "bootstrap (DOMContentLoaded / ready)" #E7E7FF {
:if document.readyState==='loading' -> on DOMContentLoaded: new AIRepoCommander().initialize();\nelse: new AIRepoCommander().initialize();\nAI_REPO_DETECTOR?.start();
kill
}
end fork
@enduml
' ==== METHOD: AIRepoCommander.constructor() =================================
@startuml
title main:constructor(): \n Prepare state and assistant selectors
participant "Caller" as CL
participant "constructor()" as CTOR
activate CL
CL -> CTOR : new AIRepoCommander()
activate CTOR
CTOR -> CTOR : isInitialized=false; observer=null
CTOR -> CTOR : processed=new WeakSet()
CTOR -> CTOR : messageSelectors=[ '[data-message-author-role=\"assistant\"]', '.chat-message:not([data-message-author-role=\"user\"])', '.message-content' ]
CTOR --> CL : instance
deactivate CTOR
deactivate CL
@enduml
' ==== METHOD: initialize() ==================================================
@startuml
title main:initialize(): \n Log config, start observer, optionally scan, expose API
participant "Caller" as CL
participant "initialize()" as INIT
participant "Logger" as LOG
participant "Config" as CFG
participant "startObserver()" as OBS
participant "scanExisting()" as SCN
participant "exposeAPI()" as EXP
activate CL
CL -> INIT : initial request
activate INIT
alt already initialized
INIT -> LOG : warn('Already initialized, skipping')
LOG --> INIT : ok
INIT --> CL : (void)
deactivate INIT
deactivate CL
return
end
INIT -> LOG : info('AI Repo Commander initializing', {version, debugLevel, apiEnabled})
LOG --> INIT : ok
INIT -> LOG : verbose('Configuration summary', {...})
LOG --> INIT : ok
INIT -> OBS : startObserver()
OBS --> INIT : observing
INIT -> CFG : get('ui.processExisting')
CFG --> INIT : boolean
alt processExisting == true
INIT -> LOG : verbose('Will process existing messages on page')
LOG --> INIT : ok
INIT -> SCN : scanExisting()
SCN --> INIT : done
end
INIT -> EXP : exposeAPI()
EXP --> INIT : exposed
INIT -> INIT : isInitialized = true
INIT -> LOG : info('AI Repo Commander initialized')
LOG --> INIT : ok
INIT -> LOG : trace('Exposed globals:', Object.keys(window).filter(k => k.startsWith('AI_REPO')))
LOG --> INIT : ok
INIT --> CL : (void)
deactivate INIT
deactivate CL
@enduml
' ==== METHOD: startObserver() ==============================================
@startuml
title main:startObserver(): \n Watch document for assistant messages and process them
participant "Caller" as CL
participant "startObserver()" as SOB
participant "MutationObserver" as MO
participant "Logger" as LOG
participant "Config" as CFG
participant "processMessage(el)" as PRO
participant "DOM" as DOM
activate CL
CL -> SOB : initial request
activate SOB
SOB -> MO : new MutationObserver(callback)
MO --> SOB : observer
' begin observing
SOB -> DOM : observer.observe(document.body, {childList:true, subtree:true})
DOM --> SOB : ok
SOB -> LOG : verbose('MutationObserver started, watching document.body')
LOG --> SOB : ok
' callback behavior (sketch)
SOB -> CFG : get('runtime.paused')
CFG --> SOB : paused?
alt paused
SOB -> LOG : trace('Mutations ignored (paused)')
else active
SOB -> SOB : assistantMsgCount = 0
' for each m of mutations
SOB -> SOB : for addedNodes: if element & isAssistantMessage(n) -> ++count; processMessage(n);\nscan n.querySelectorAll(...) and process each
SOB -> LOG : verbose(`Detected ${assistantMsgCount} assistant message(s)`) ' only if >0
end
SOB --> CL : (void)
deactivate SOB
deactivate CL
@enduml
' ==== METHOD: isAssistantMessage(el) =======================================
@startuml
title main:isAssistantMessage(el): \n Match against known assistant selectors
participant "Caller" as CL
participant "isAssistantMessage(el)" as IAM
activate CL
CL -> IAM : el
activate IAM
IAM -> IAM : return messageSelectors.some(sel => el.matches?.(sel))
IAM --> CL : true/false
deactivate IAM
deactivate CL
@enduml
' ==== METHOD: processMessage(el) ===========================================
@startuml
title main:processMessage(el): \n Dedup, extract commands, cap, and run or add retry
participant "Caller" as CL
participant "processMessage(el)" as PRO
participant "Logger" as LOG
participant "Config" as CFG
participant "History" as HIS
participant "extractCommands(el)" as EXT
participant "run(el,cmd,idx)" as RUN
participant "addRetryButton(el,txt,idx)" as ARB
activate CL
CL -> PRO : el
activate PRO
alt processed.has(el)
PRO -> LOG : trace('Message already processed, skipping')
LOG --> PRO : ok
PRO --> CL : (void)
deactivate PRO
deactivate CL
return
end
PRO -> EXT : extractCommands(el)
EXT --> PRO : commands[]
alt commands.length == 0
PRO -> LOG : trace('No commands found in message')
LOG --> PRO : ok
PRO --> CL : (void)
deactivate PRO
deactivate CL
return
end
PRO -> LOG : verbose(`Found ${commands.length} command block(s) in message`)
LOG --> PRO : ok
PRO -> PRO : processed.add(el)
PRO -> CFG : get('queue.maxPerMessage')
CFG --> PRO : maxPerMsg
PRO -> PRO : toProcess = commands.slice(0, maxPerMsg)
alt commands.length > maxPerMsg
PRO -> LOG : warn(`Message has ${commands.length} commands, limiting to first ${maxPerMsg}`)
LOG --> PRO : ok
end
loop for each (cmdText, idx) in toProcess
PRO -> HIS : isProcessed(el, idx)
HIS --> PRO : bool
alt already processed
PRO -> LOG : verbose(`Command #${idx+1} already executed, adding retry button`)
LOG --> PRO : ok
PRO -> ARB : addRetryButton(el, cmdText, idx)
ARB --> PRO : button added
else not processed
PRO -> LOG : verbose(`Queueing command #${idx+1} for execution`)
LOG --> PRO : ok
PRO -> RUN : run(el, cmdText, idx)
RUN --> PRO : (void)
end
end
PRO --> CL : (void)
deactivate PRO
deactivate CL
@enduml
' ==== METHOD: extractCommands(el) ==========================================
@startuml
title main:extractCommands(el): \n Collect @bridge@...@end@ blocks with a global regex
participant "Caller" as CL
participant "extractCommands(el)" as EXT
activate CL
CL -> EXT : el
activate EXT
EXT -> EXT : text = el.textContent || ''
EXT -> EXT : out=[]; re=/@bridge@[\\s\\S]*?@end@/g; while (m=re.exec(text)) out.push(m[0])
EXT --> CL : out
deactivate EXT
deactivate CL
@enduml
' ==== METHOD: run(el, commandText, index) ==================================
@startuml
title main:run(el, commandText, index): \n Parse, validate, optional debounce, execute via executor
participant "Caller" as CL
participant "run(el,commandText,index)" as RUN
participant "Logger" as LOG
participant "History" as HIS
participant "Parser.parse()" as PAR
participant "Parser.validate()" as VAL
participant "Config" as CFG
participant "Executor.execute()" as EXE
participant "Timer" as TMR
activate CL
CL -> RUN : el, commandText, index
activate RUN
RUN -> LOG : trace(`Starting run() for #${index+1}`, { preview })
LOG --> RUN : ok
RUN -> HIS : markProcessed(el, index)
HIS --> RUN : ok
RUN -> PAR : parse(commandText)
PAR --> RUN : parsed
RUN -> LOG : verbose(`Parsed command #${index+1}:`, { action:parsed.action, repo:parsed.repo, path:parsed.path })
LOG --> RUN : ok
RUN -> VAL : validate(parsed)
VAL --> RUN : validation
alt !validation.isValid
RUN -> LOG : error('Command validation failed', { errors, command:parsed.action })
LOG --> RUN : ok
RUN -> RUN : addRetryButton(el, commandText, index)
RUN --> CL : (void)
deactivate RUN
deactivate CL
return
else validation.example
RUN -> LOG : info('Skipping example command', { action: parsed.action })
LOG --> RUN : ok
RUN --> CL : (void)
deactivate RUN
deactivate CL
return
end
RUN -> CFG : get('execution.debounceDelay') || 0
CFG --> RUN : debounce
alt debounce > 0
RUN -> LOG : trace(`Debouncing for ${debounce}ms before execution`)
LOG --> RUN : ok
RUN -> TMR : setTimeout(debounce)
TMR --> RUN : elapsed
end
RUN -> LOG : verbose(`Executing command #${index+1}: ${parsed.action}`)
LOG --> RUN : ok
RUN -> EXE : execute(parsed, el, `Command ${index+1}`)
EXE --> RUN : result
RUN -> LOG : verbose(`Command #${index+1} completed successfully`)
LOG --> RUN : ok
RUN --> CL : (void)
deactivate RUN
deactivate CL
@enduml
' ==== METHOD: addRetryButton(el, commandText, idx) =========================
@startuml
title main:addRetryButton(el, commandText, idx): \n Append a Run Again button bound to run()
participant "Caller" as CL
participant "addRetryButton(el,txt,idx)" as ARB
participant "DOM" as DOM
participant "run(el,txt,idx)" as RUN
activate CL
CL -> ARB : el, commandText, idx
activate ARB
ARB -> DOM : createElement('button'); set text, styles
DOM --> ARB : btn
ARB -> DOM : btn.addEventListener('click', () => RUN(...))
DOM --> ARB : wired
ARB -> DOM : el.appendChild(btn)
DOM --> ARB : appended
ARB --> CL : (void)
deactivate ARB
deactivate CL
@enduml
' ==== METHOD: scanExisting() ===============================================
@startuml
title main:scanExisting(): \n Walk current DOM for assistant messages and process them
participant "Caller" as CL
participant "scanExisting()" as SCN
participant "Logger" as LOG
participant "DOM" as DOM
participant "isAssistantMessage(el)" as IAM
participant "processMessage(el)" as PRO
activate CL
CL -> SCN : initial request
activate SCN
SCN -> DOM : querySelectorAll(messageSelectors.join(','))
DOM --> SCN : nodes[]
SCN -> LOG : verbose(`Scanning ${nodes.length} existing message(s) on page`)
LOG --> SCN : ok
SCN -> SCN : processed=0
loop for each el in nodes
SCN -> IAM : isAssistantMessage(el)
IAM --> SCN : bool
alt true
SCN -> SCN : processed++
SCN -> PRO : processMessage(el)
PRO --> SCN : done
end
end
SCN -> LOG : info(`Scanned ${processed} existing assistant message(s)`)
LOG --> SCN : ok
SCN --> CL : (void)
deactivate SCN
deactivate CL
@enduml
' ==== METHOD: exposeAPI() ===================================================
@startuml
title main:exposeAPI(): \n Publish helpers on window: AI_REPO, AI_REPO_STOP, AI_REPO_SET_KEY
participant "Caller" as CL
participant "exposeAPI()" as EXP
participant "window" as WIN
participant "Config" as CFG
participant "Logger" as LOG
participant "Queue" as QUE
participant "History" as HIS
activate CL
CL -> EXP : initial request
activate EXP
' AI_REPO object
EXP -> CFG : get('meta.version')
CFG --> EXP : version
EXP -> WIN : WIN.AI_REPO = { version, config:CFG, logger:LOG, history:HIS, pause(), resume(), clearHistory() }
WIN --> EXP : ok
' STOP function
EXP -> WIN : WIN.AI_REPO_STOP = () => { CFG.set('api.enabled', false); CFG.set('runtime.paused', true); const n = QUE.size()?; QUE.clear()?; LOG.error('🚨 EMERGENCY STOP...', {n}); LOG.error('API disabled and scanning paused'); }
WIN --> EXP : ok
' BridgeKey setter
EXP -> WIN : WIN.AI_REPO_SET_KEY = (k) => { if (string && trim) CFG.set('api.bridgeKey', trim); LOG.info('Bridge key updated'); return true; else LOG.warn('Invalid bridge key'); return false; }
WIN --> EXP : ok
EXP --> CL : (void)
deactivate EXP
deactivate CL
@enduml
' ==== METHOD: delay(ms) =====================================================
@startuml
title main:delay(ms): \n Promise that resolves after ms
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 : elapsed
DLY --> CL : Promise resolved
deactivate DLY
deactivate CL
@enduml
' ==== METHOD: destroy() =====================================================
@startuml
title main:destroy(): \n Disconnect observer, reset flags, log teardown
participant "Caller" as CL
participant "destroy()" as DST
participant "Logger" as LOG
activate CL
CL -> DST : initial request
activate DST
DST -> DST : observer?.disconnect()
DST -> DST : processed = new WeakSet()
DST -> DST : isInitialized = false
DST -> LOG : info('AI Repo Commander destroyed')
LOG --> DST : ok
DST --> CL : (void)
deactivate DST
deactivate CL
@enduml
' ==== METHOD: bootstrap (DOMContentLoaded / ready) ==========================
@startuml
title main:bootstrap: \n Instantiate and initialize; start advanced detector when ready
participant "Caller" as CL
participant "bootstrap" as BOOT
participant "DOM" as DOM
participant "AIRepoCommander()" as CTOR
participant "initialize()" as INIT
participant "Detector.start()" as DET
activate CL
CL -> BOOT : module load
activate BOOT
BOOT -> DOM : document.readyState
DOM --> BOOT : 'loading' | 'complete'
alt loading
BOOT -> DOM : addEventListener('DOMContentLoaded', () => { WIN.AI_REPO_MAIN = new AIRepoCommander(); AI_REPO_MAIN.initialize(); })
DOM --> BOOT : listener attached
else ready
BOOT -> CTOR : new AIRepoCommander()
CTOR --> BOOT : instance
BOOT -> DOM : WIN.AI_REPO_MAIN = instance
BOOT -> INIT : AI_REPO_MAIN.initialize()
INIT --> BOOT : initialized
BOOT -> DET : AI_REPO_DETECTOR?.start()
DET --> BOOT : started | skipped
end
BOOT --> CL : (void)
deactivate BOOT
deactivate CL
@enduml
' ==== LEGEND ===============================================================
@startuml
legend bottom
== main 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 class lifeline; the class/module name appears in the title only.
3) Include every directly-called helper/system as a participant
(e.g., "Logger", "Config", "History", "Parser", "Executor", "Queue", "MutationObserver", "Detector", "DOM", "Timer", "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 method.
B) One partition per method; soft background color; terminate branches with 'kill'.
C) Keep wording aligned with code (e.g., debounceDelay, maxPerMessage, selector list, detector.start in ready path).
• Color palette (soft pastels)
• Use --> for returns; -> for calls.
• Participants use quoted method names for internals, plain nouns for systems ("DOM", "Timer", "MutationObserver", "Detector").
• Keep this legend at the end of the file to standardize edits.
endlegend
@enduml

View File

@ -0,0 +1,398 @@
' ===================================================================
' File: paste-submit.puml
' Purpose: Single source of truth for module-level activity + per-function sequences.
' Module: paste-submit.js — Find composer, paste text robustly, and optionally submit.
' 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 (paste-submit.js) ==================================
@startuml
title paste-submit.js — Branch Flow (full module)
start
:paste-submit;
fork
' -------- findComposer() --------
partition "findComposer()" #E7FAE3 {
:findComposer;
:iterate selectors in priority order;\nskip hidden/disabled/offsetParent null (unless fixed);
:return first visible editor or null;
kill
}
fork again
' -------- findSendButton(scopeEl) --------
partition "findSendButton(scopeEl)" #FFF6D1 {
:findSendButton;
:scope = closest(form/composer/main/body);\nquery candidate buttons; filter disabled/hidden;
:return button or null;
kill
}
fork again
' -------- pressEnter(el) --------
partition "pressEnter(el)" #FFE1DB {
:pressEnter;
:dispatch keydown/keypress/keyup Enter;\nreturn success boolean;
kill
}
fork again
' -------- waitReady(timeoutMs) --------
partition "waitReady(timeoutMs)" #DCF9EE {
:waitReady;
:loop until timeout: findComposer();\nensure not busy and editor empty;
:return true/false;
kill
}
fork again
' -------- pasteInto(el, text) --------
partition "pasteInto(el, text)" #FFE6F0 {
:pasteInto;
:payload = maybe add trailing newline (cfg.ui.appendTrailingNewline);
:try ClipboardEvent path -> return true;
:if ProseMirror -> set text + input -> true;
:if contentEditable -> selection insert + input -> true;
:if textarea/input -> value + input -> true;
:fallback GM_setClipboard + alert -> true;
:else return false;
kill
}
fork again
' -------- submitToComposer(text) --------
partition "submitToComposer(text)" #E6F3FF {
:submitToComposer;
:auto = cfg.ui.autoSubmit;\nok = waitReady(...);\nif !ok -> warn + false;
:el = findComposer(); pasteInto if text;\npostPasteDelay; if !auto -> true;
:btn = findSendButton(el)? click : pressEnter(el);
:return boolean;
kill
}
end fork
@enduml
' ==== METHOD: findComposer() ===============================================
@startuml
title paste-submit:findComposer(): \n Locate a visible, interactable chat editor element
participant "Caller" as CL
participant "findComposer()" as FC
participant "DOM" as DOM
activate CL
CL -> FC : initial request
activate FC
FC -> FC : for s in selectors (priority list)
FC -> DOM : querySelector(s)
DOM --> FC : el | null
alt el found
FC -> FC : st = getComputedStyle(el)\nif hidden/visibility:hidden -> continue\nif offsetParent==null && position!='fixed' -> continue
alt visible + interactable
FC --> CL : el
deactivate FC
deactivate CL
return
else skip
end
end
FC --> CL : null
deactivate FC
deactivate CL
@enduml
' ==== METHOD: findSendButton(scopeEl) ======================================
@startuml
title paste-submit:findSendButton(scopeEl): \n Find a nearby, visible Send/Submit button
participant "Caller" as CL
participant "findSendButton(scopeEl)" as FSB
participant "DOM" as DOM
activate CL
CL -> FSB : scopeEl
activate FSB
FSB -> DOM : scope = scopeEl.closest('form,[data-testid="composer"],main,body') || document
DOM --> FSB : scope
FSB -> FSB : for s in candidate selectors
FSB -> DOM : scope.querySelector(s) || document.querySelector(s)
DOM --> FSB : b | null
alt b found
FSB -> FSB : st = getComputedStyle(b); disabled/hidden/offsetParent?
alt clickable
FSB --> CL : b
deactivate FSB
deactivate CL
return
else continue
end
end
FSB --> CL : null
deactivate FSB
deactivate CL
@enduml
' ==== METHOD: pressEnter(el) ===============================================
@startuml
title paste-submit:pressEnter(el): \n Simulate Enter keystrokes against the editor
participant "Caller" as CL
participant "pressEnter(el)" as PE
participant "DOM" as DOM
activate CL
CL -> PE : el
activate PE
loop for t in ['keydown','keypress','keyup']
PE -> DOM : dispatch KeyboardEvent(t,{Enter, bubbles:true, cancelable:true})
DOM --> PE : ok (boolean)
alt not ok
PE --> CL : false
deactivate PE
deactivate CL
return
end
end
PE --> CL : true
deactivate PE
deactivate CL
@enduml
' ==== METHOD: waitReady(timeoutMs) =========================================
@startuml
title paste-submit:waitReady(timeoutMs): \n Ensure composer exists, is idle, and empty
participant "Caller" as CL
participant "waitReady(timeoutMs)" as WR
participant "findComposer()" as FC
participant "DOM" as DOM
participant "Timer" as TMR
activate CL
CL -> WR : timeoutMs
activate WR
WR -> WR : start = now
loop until now - start < timeoutMs
WR -> FC : findComposer()
FC --> WR : el | null
alt el present
WR -> WR : current = (el.textContent || el.value || '').trim()
WR -> DOM : search closest(...).querySelector('[aria-busy="true"],[data-state="loading"],.typing-indicator')
DOM --> WR : busyEl | null
alt !busyEl && current.length == 0
WR --> CL : true
deactivate WR
deactivate CL
return
end
end
WR -> TMR : setTimeout(200ms)
TMR --> WR : tick
end
WR --> CL : false
deactivate WR
deactivate CL
@enduml
' ==== METHOD: pasteInto(el, text) ==========================================
@startuml
title paste-submit:pasteInto(el, text): \n Paste via multiple strategies from cleanest to fallback
participant "Caller" as CL
participant "pasteInto(el, text)" as PI
participant "Config" as CFG
participant "DOM" as DOM
participant "GM_setClipboard" as CLP
activate CL
CL -> PI : el, text
activate PI
' payload prep
PI -> CFG : get('ui.appendTrailingNewline')
CFG --> PI : boolean
PI -> PI : payload = (append?\n text.endsWith('\\n')?text:text+'\\n' : text)
' Strategy 1: ClipboardEvent path
PI -> PI : try { dt = new DataTransfer(); dt.setData('text/plain', payload) }
PI -> DOM : dispatch ClipboardEvent('paste', {clipboardData:dt,bubbles:true,cancelable:true})
DOM --> PI : dispatched/blocked
alt event accepted and not defaultPrevented
PI --> CL : true
deactivate PI
deactivate CL
return
end
' Strategy 2: ProseMirror
PI -> PI : if el.classList.contains('ProseMirror')\n el.innerHTML=''; append text node; dispatch 'input'
alt ProseMirror handled
PI --> CL : true
deactivate PI
deactivate CL
return
end
' Strategy 3: contentEditable selection
PI -> PI : if el.isContentEditable or contenteditable='true'
PI -> DOM : ensure selection range at end
DOM --> PI : range | null
alt range present
PI -> DOM : range.deleteContents(); insertTextNode(payload); move caret; dispatch 'input'
DOM --> PI : ok
PI --> CL : true
deactivate PI
deactivate CL
return
end
' Strategy 4: textarea/input
PI -> PI : if tagName in {TEXTAREA, INPUT}\n el.value=payload; dispatch 'input'
alt text control handled
PI --> CL : true
deactivate PI
deactivate CL
return
end
' Strategy 5: clipboard fallback
PI -> CLP : GM_setClipboard(payload, {type:'text',mimetype:'text/plain'})
CLP --> PI : ok | error
alt ok
PI -> DOM : alert('Content copied to clipboard. Press Ctrl/Cmd+V to paste.')
DOM --> PI : shown
PI --> CL : true
else failed
PI --> CL : false
end
deactivate PI
deactivate CL
@enduml
' ==== METHOD: submitToComposer(text) =======================================
@startuml
title paste-submit:submitToComposer(text): \n Wait until ready, paste text, and optionally auto-submit
participant "Caller" as CL
participant "submitToComposer(text)" as SUB
participant "Config" as CFG
participant "Logger" as LOG
participant "waitReady(timeoutMs)" as WR
participant "findComposer()" as FC
participant "pasteInto(el, text)" as PI
participant "findSendButton(scopeEl)" as FSB
participant "pressEnter(el)" as PE
participant "Timer" as TMR
activate CL
CL -> SUB : text
activate SUB
' readiness
SUB -> CFG : get('ui.autoSubmit')
CFG --> SUB : auto (bool)
SUB -> CFG : get('execution.settleCheckMs') || 1200
CFG --> SUB : settleMs
SUB -> WR : waitReady(settleMs)
WR --> SUB : ok (bool)
alt !ok
SUB -> LOG : warn("Composer not ready")
SUB --> CL : false
deactivate SUB
deactivate CL
return
end
' find composer + paste
SUB -> FC : findComposer()
FC --> SUB : el | null
alt el is null
SUB -> LOG : warn("Composer not found")
SUB --> CL : false
deactivate SUB
deactivate CL
return
end
alt text provided
SUB -> PI : pasteInto(el, text)
PI --> SUB : pasted (bool)
alt !pasted
SUB -> LOG : warn("Paste failed")
SUB --> CL : false
deactivate SUB
deactivate CL
return
end
end
' post-paste delay
SUB -> CFG : get('ui.postPasteDelayMs') || 600
CFG --> SUB : postMs
SUB -> TMR : setTimeout(postMs)
TMR --> SUB : wake
alt auto == false
SUB --> CL : true
deactivate SUB
deactivate CL
return
end
' submit by button or enter
SUB -> FSB : findSendButton(el)
FSB --> SUB : btn | null
alt btn found
SUB -> SUB : btn.click()
SUB --> CL : true
else no button
SUB -> PE : pressEnter(el)
PE --> SUB : ok (bool)
SUB --> CL : ok
end
deactivate SUB
deactivate CL
@enduml
' ==== LEGEND ===============================================================
@startuml
legend bottom
== paste-submit 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., "findComposer()", "pasteInto()", "findSendButton()", "submitToComposer()", "pressEnter()", "waitReady()", "Config", "Logger", "DOM", "Timer", "GM_setClipboard").
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., selector heuristics, delay knobs, config flags).
• Color palette (soft pastels)
• Use --> for returns; -> for calls.
• Participants use quoted method names for internals (e.g., "pasteInto()"), and plain nouns for systems ("DOM", "GM_setClipboard", "Timer").
• Keep this legend at the end of the file to standardize edits.
endlegend
@enduml

280
Docs/diagrams/queue.puml Normal file
View File

@ -0,0 +1,280 @@
' ===================================================================
' File: queue.puml
' Purpose: Single source of truth for module-level activity + per-method sequences.
' Module: queue.js — Rate-limited FIFO queue (min delay, max per minute), async drain.
' 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 (queue.js) =========================================
@startuml
title queue.js — Branch Flow (full module)
start
:queue;
fork
' -------- constructor(opts) --------
partition "constructor(opts)" #E7FAE3 {
:constructor;
:minDelayMs = opts.minDelayMs ?? cfg.queue.minDelayMs ?? 1500;
:maxPerMinute = opts.maxPerMinute ?? cfg.queue.maxPerMinute ?? 15;
:q=[]; running=false; timestamps=[]; onSizeChange=null;
kill
}
fork again
' -------- push(task) --------
partition "push(task)" #FFF6D1 {
:push;
:q.push(task); onSizeChange?.(q.length);
:if !running -> _drain();
kill
}
fork again
' -------- clear() --------
partition "clear()" #FFE1DB {
:clear;
:q.length = 0; onSizeChange?.(0);
kill
}
fork again
' -------- size() --------
partition "size()" #DCF9EE {
:size;
:return q.length;
kill
}
fork again
' -------- _withinBudget() --------
partition "_withinBudget()" #FFE6F0 {
:_withinBudget;
:timestamps = timestamps.filter(now - t < 60000);
:return timestamps.length < maxPerMinute;
kill
}
fork again
' -------- _drain() --------
partition "_drain()" #E6F3FF {
:_drain;
:guard running; set running=true;
:while q.length > 0;
:while !_withinBudget -> _delay(400);
:fn = q.shift(); onSizeChange?.(q.length);
:try await fn(); catch -> log.warn("Queue task error");
:timestamps.push(Date.now());
:await _delay(minDelayMs);
:end while; running=false;
kill
}
fork again
' -------- _delay(ms) --------
partition "_delay(ms)" #F0E6FA {
:_delay;
:return new Promise(resolve after setTimeout(ms));
kill
}
end fork
@enduml
' ==== METHOD: constructor(opts) ============================================
@startuml
title queue:constructor(opts): \n Initialize rate limits, internal state, and callbacks
participant "Caller" as CL
participant "constructor(opts)" as CTOR
participant "Config" as CFG
activate CL
CL -> CTOR : new ExecutionQueue(opts)
activate CTOR
CTOR -> CFG : get('queue.minDelayMs') / get('queue.maxPerMinute')
CFG --> CTOR : minCfg / maxCfg
CTOR -> CTOR : minDelayMs = opts.minDelayMs ?? minCfg ?? 1500
CTOR -> CTOR : maxPerMinute = opts.maxPerMinute ?? maxCfg ?? 15
CTOR -> CTOR : q=[]; running=false; timestamps=[]; onSizeChange=null
CTOR --> CL : instance
deactivate CTOR
deactivate CL
@enduml
' ==== METHOD: push(task) ====================================================
@startuml
title queue:push(task): \n Enqueue a task and start the drain loop if idle
participant "Caller" as CL
participant "push(task)" as PUSH
participant "_drain()" as DRN
activate CL
CL -> PUSH : task (async function)
activate PUSH
PUSH -> PUSH : q.push(task)
PUSH -> PUSH : onSizeChange?.(q.length)
PUSH -> PUSH : if (!running) -> start drain
PUSH -> DRN : _drain()
DRN --> PUSH : (started | already running)
PUSH --> CL : (void)
deactivate PUSH
deactivate CL
@enduml
' ==== METHOD: clear() =======================================================
@startuml
title queue:clear(): \n Drop all pending tasks and notify size change
participant "Caller" as CL
participant "clear()" as CLR
activate CL
CL -> CLR : initial request
activate CLR
CLR -> CLR : q.length = 0
CLR -> CLR : onSizeChange?.(0)
CLR --> CL : (void)
deactivate CLR
deactivate CL
@enduml
' ==== METHOD: size() ========================================================
@startuml
title queue:size(): \n Return current queue length
participant "Caller" as CL
participant "size()" as SIZE
activate CL
CL -> SIZE : initial request
activate SIZE
SIZE --> CL : q.length
deactivate SIZE
deactivate CL
@enduml
' ==== METHOD: _withinBudget() ==============================================
@startuml
title queue:_withinBudget(): \n Enforce rolling 60s window and max tasks per minute
participant "Caller" as CL
participant "_withinBudget()" as WBG
activate CL
CL -> WBG : initial request
activate WBG
WBG -> WBG : now = Date.now()
WBG -> WBG : timestamps = timestamps.filter(now - t < 60000)
WBG --> CL : (timestamps.length < maxPerMinute)
deactivate WBG
deactivate CL
@enduml
' ==== METHOD: _drain() ======================================================
@startuml
title queue:_drain(): \n Process tasks while respecting rate limits and min spacing
participant "Caller" as CL
participant "_drain()" as DRN
participant "_withinBudget()" as WBG
participant "_delay(ms)" as DLY
participant "Logger" as LOG
activate CL
CL -> DRN : initial request
activate DRN
' guard
DRN -> DRN : if (running) return
DRN -> DRN : running = true
loop while q.length > 0
' wait for budget
DRN -> WBG : _withinBudget()
WBG --> DRN : ok (bool)
alt !ok
DRN -> DLY : _delay(400)
DLY --> DRN : wake
DRN -> WBG : _withinBudget()
WBG --> DRN : ok (bool)
end
' get next task
DRN -> DRN : fn = q.shift()
DRN -> DRN : onSizeChange?.(q.length)
' execute task safely
alt try/await fn()
DRN -> DRN : await fn()
else error
DRN -> LOG : warn("Queue task error", {error})
end
' record + spacing
DRN -> DRN : timestamps.push(Date.now())
DRN -> DLY : _delay(minDelayMs)
DLY --> DRN : wake
end
DRN -> DRN : running = false
DRN --> CL : (void)
deactivate DRN
deactivate CL
@enduml
' ==== METHOD: _delay(ms) ====================================================
@startuml
title queue:_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
== queue 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., "push()", "clear()", "size()", "_withinBudget()", "_drain()", "_delay()", "Logger", "Config", "Timer").
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., 60_000s window, minDelayMs, maxPerMinute).
• Color palette (soft pastels)
• Use --> for returns; -> for calls.
• Participants use quoted method names for internals (e.g., "_drain()"), and plain nouns for systems ("Logger", "Timer").
• Keep this legend at the end of the file to standardize edits.
endlegend
@enduml

View File

@ -0,0 +1,313 @@
' ===================================================================
' File: response-buffer.puml
' Purpose: Single source of truth for module-level activity + per-method sequences.
' Module: response-buffer.js — Buffer result chunks; split safely; enqueue paste ops.
' 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 (response-buffer.js) ===============================
@startuml
title response-buffer.js — Branch Flow (full module)
start
:response-buffer;
fork
' -------- chunkByLines(s, limit) --------
partition "chunkByLines(s, limit)" #E7FAE3 {
:chunkByLines;
:walk by soft breaks at \\n near limit;\nappend slice; advance;
:return chunks[];
kill
}
fork again
' -------- isSingleFence(s) --------
partition "isSingleFence(s)" #FFF6D1 {
:isSingleFence;
:/^```[^\\n]*\\n[\\s\\S]*\\n```$/ on trim(s);
:return boolean;
kill
}
fork again
' -------- splitRespectingFence(text, limit) --------
partition "splitRespectingFence(text, limit)" #FFE1DB {
:splitRespectingFence;
:if not single fence -> chunkByLines(text, limit);\nelse split inner with adjusted limit;\nre-wrap each in same fence;
:return chunks[];
kill
}
fork again
' -------- ResponseBuffer.constructor() --------
partition "ResponseBuffer.constructor()" #DCF9EE {
:constructor;
:pending=[]; timer=null; flushing=false;
kill
}
fork again
' -------- ResponseBuffer.push({label,content}) --------
partition "push({label,content})" #FFE6F0 {
:push;
:ignore if no content; pending.push({label,content});\n_schedule();
kill
}
fork again
' -------- ResponseBuffer._schedule() --------
partition "_schedule()" #E6F3FF {
:_schedule;
:clearTimeout(timer);\n timer = setTimeout(() => flush(), 500);
kill
}
fork again
' -------- ResponseBuffer._build() --------
partition "_build()" #F0E6FA {
:_build;
:showHeadings=true; concat "### label" + content blocks;\nreturn big string;
kill
}
fork again
' -------- ResponseBuffer.flush() --------
partition "flush()" #E7FAF7 {
:flush;
:guard: if flushing or empty -> return;\nflushing=true; toPaste=_build(); pending.length=0;
:if toPaste.length > limit -> splitRespectingFence(...);\n enqueue each Part i payload;\nelse enqueue whole payload;
:finally flushing=false;
kill
}
end fork
@enduml
' ==== METHOD: chunkByLines(s, limit) =======================================
@startuml
title response-buffer:chunkByLines(s, limit): \n Split on newline boundaries near the limit
participant "Caller" as CL
participant "chunkByLines(s, limit)" as CBL
activate CL
CL -> CBL : s, limit
activate CBL
CBL -> CBL : out=[]; start=0
loop while start < s.length
CBL -> CBL : soft = s.lastIndexOf('\\n', min(start+limit, s.length))
CBL -> CBL : end = (soft > start ? soft+1 : min(start+limit, s.length))
CBL -> CBL : out.push(s.slice(start,end)); start=end
end
CBL --> CL : chunks[]
deactivate CBL
deactivate CL
@enduml
' ==== METHOD: isSingleFence(s) =============================================
@startuml
title response-buffer:isSingleFence(s): \n Detect single fenced block (```lang ... ```)
participant "Caller" as CL
participant "isSingleFence(s)" as ISF
activate CL
CL -> ISF : s
activate ISF
ISF -> ISF : test /^```[^\\n]*\\n[\\s\\S]*\\n```$/ on s.trim()
ISF --> CL : true/false
deactivate ISF
deactivate CL
@enduml
' ==== METHOD: splitRespectingFence(text, limit) =============================
@startuml
title response-buffer:splitRespectingFence(text, limit): \n Chunk text, keeping fenced blocks intact
participant "Caller" as CL
participant "splitRespectingFence(text, limit)" as SRF
participant "isSingleFence(s)" as ISF
participant "chunkByLines(s, limit)" as CBL
activate CL
CL -> SRF : text, limit
activate SRF
SRF -> SRF : t = text.trim()
SRF -> ISF : isSingleFence(t)
ISF --> SRF : single (bool)
alt single == false
SRF -> CBL : chunkByLines(text, limit)
CBL --> SRF : chunks[]
SRF --> CL : chunks[]
else single == true
SRF -> SRF : m = /^```([^\\n]*)\\n([\\s\\S]*)\\n```$/.exec(t)\nlang=(m?.[1]||'text').trim(); inner=m?.[2]||''
SRF -> SRF : innerLimit = limit - 16 - lang.length
SRF -> CBL : chunkByLines(inner, innerLimit)
CBL --> SRF : innerChunks[]
SRF -> SRF : chunks = innerChunks.map(c => '```'+lang+'\\n'+c.replace(/\\n?$/,'\\n')+'```')
SRF --> CL : chunks
end
deactivate SRF
deactivate CL
@enduml
' ==== METHOD: ResponseBuffer.constructor() ==================================
@startuml
title response-buffer:constructor(): \n Initialize pending buffer, timer, and state
participant "Caller" as CL
participant "constructor()" as CTOR
activate CL
CL -> CTOR : new ResponseBuffer()
activate CTOR
CTOR -> CTOR : pending=[]; timer=null; flushing=false
CTOR --> CL : instance
deactivate CTOR
deactivate CL
@enduml
' ==== METHOD: ResponseBuffer.push({label, content}) =========================
@startuml
title response-buffer:push({label, content}): \n Collect a piece for later batched paste
participant "Caller" as CL
participant "push({label,content})" as PUSH
participant "_schedule()" as SCH
activate CL
CL -> PUSH : {label, content}
activate PUSH
alt !content
PUSH --> CL : (void)
deactivate PUSH
deactivate CL
return
end
PUSH -> PUSH : pending.push({label,content})
PUSH -> SCH : _schedule()
SCH --> PUSH : scheduled
PUSH --> CL : (void)
deactivate PUSH
deactivate CL
@enduml
' ==== METHOD: ResponseBuffer._schedule() ====================================
@startuml
title response-buffer:_schedule(): \n Debounce flush with a short timer
participant "Caller" as CL
participant "_schedule()" as SCH
participant "Timer" as TMR
activate CL
CL -> SCH : initial request
activate SCH
SCH -> TMR : clearTimeout(timer)
TMR --> SCH : cleared
SCH -> TMR : timer = setTimeout( flush, 500 )
TMR --> SCH : id
SCH --> CL : (void)
deactivate SCH
deactivate CL
@enduml
' ==== METHOD: ResponseBuffer._build() =======================================
@startuml
title response-buffer:_build(): \n Build a readable combined payload with headings
participant "Caller" as CL
participant "_build()" as BLD
activate CL
CL -> BLD : initial request
activate BLD
BLD -> BLD : showHeadings = true; parts=[]
loop for each {label,content} in pending
BLD -> BLD : if label -> parts.push('### '+label+'\\n')\nparts.push(String(content).trimEnd(), '')
end
BLD --> CL : parts.join('\\n')
deactivate BLD
deactivate CL
@enduml
' ==== METHOD: ResponseBuffer.flush() ========================================
@startuml
title response-buffer:flush(): \n Debounced paste: split if huge, enqueue parts on ExecutionQueue
participant "Caller" as CL
participant "flush()" as FLS
participant "_build()" as BLD
participant "splitRespectingFence(text, limit)" as SRF
participant "Queue" as QUE
participant "Paste.submitToComposer(text)" as PST
activate CL
CL -> FLS : initial request
activate FLS
' guard + state
FLS -> FLS : if (flushing || pending.length==0) return
FLS -> FLS : flushing = true
FLS -> BLD : _build()
BLD --> FLS : toPaste
FLS -> FLS : pending.length = 0
' branch by length limit
FLS -> FLS : limit = 250_000
alt toPaste.length > limit
FLS -> SRF : splitRespectingFence(toPaste, limit)
SRF --> FLS : chunks[]
loop for each c, i
FLS -> FLS : header = `### Part ${i+1}/${chunks.length}\\n`; payload = header + c
FLS -> QUE : push(async () => PST.submitToComposer(payload))
QUE --> FLS : queued
end
else within limit
FLS -> QUE : push(async () => PST.submitToComposer(toPaste))
QUE --> FLS : queued
end
FLS --> CL : (finally flushing=false)
deactivate FLS
deactivate CL
@enduml
' ==== LEGEND ===============================================================
@startuml
legend bottom
== response-buffer 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., "chunkByLines()", "isSingleFence()", "splitRespectingFence()", "push()", "_schedule()", "_build()", "flush()", "Queue", "Paste").
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., 250_000 limit, fence shape, headings format).
• Color palette (soft pastels)
• Use --> for returns; -> for calls.
• Participants use quoted method names for internals (e.g., "flush()"), and plain nouns for systems ("Queue", "Paste", "Timer").
• Keep this legend at the end of the file to standardize edits.
endlegend
@enduml

418
Docs/diagrams/storage.puml Normal file
View File

@ -0,0 +1,418 @@
' ===================================================================
' File: storage.puml
' Purpose: Single source of truth for module-level activity + per-method sequences.
' Module: storage.js — Conversation-aware de-duplication via localStorage.
' 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 (storage.js) =======================================
@startuml
title storage.js — Branch Flow (full module)
start
:storage;
fork
' -------- ConversationHistory.constructor() --------
partition "ConversationHistory.constructor()" #E7FAE3 {
:constructor;
:conversationId = _getConversationId();\nkey = `ai_rc:conv:${conversationId}:processed`;
:cache = _load(); _cleanupExpired();
kill
}
fork again
' -------- _getConversationId() --------
partition "_getConversationId()" #FFF6D1 {
:_getConversationId;
:host = location.hostname.replace('chat.openai.com','chatgpt.com');\nreturn `${host}:${location.pathname||'/'}`;
kill
}
fork again
' -------- _load() / _save() --------
partition "_load() / _save()" #FFE1DB {
:_load -> JSON.parse(localStorage.getItem(key)||'{}')\n(catch -> {});
:_save -> localStorage.setItem(key, JSON.stringify(cache))\n(catch -> logger.warn(...));
kill
}
fork again
' -------- isProcessed(el, idx=0) --------
partition "isProcessed(el, idx=0)" #DCF9EE {
:isProcessed;
:fp = _fingerprint(el, idx);\nreturn cache.hasOwnProperty(fp);
kill
}
fork again
' -------- markProcessed(el, idx=0) --------
partition "markProcessed(el, idx=0)" #FFE6F0 {
:markProcessed;
:fp = _fingerprint(el, idx);\ncache[fp] = Date.now(); _save();\nif cfg.ui.showExecutedMarker -> _mark(el);
kill
}
fork again
' -------- _fingerprint(el, idx) --------
partition "_fingerprint(el, idx)" #E6F3FF {
:_fingerprint;
:base = (window.AI_REPO_STABLE_FINGERPRINT? stable(el)\n : window.AI_REPO_FINGERPRINT? compat(el)\n : _hash((el.textContent||'').slice(0,1000)));
:return `${base}|idx:${idx}`;
kill
}
fork again
' -------- _hash(str) --------
partition "_hash(str)" #F0E6FA {
:_hash;
:djb2-xor over up to 1000 chars;\nreturn unsigned base36;
kill
}
fork again
' -------- _mark(el) --------
partition "_mark(el)" #E7FAF7 {
:_mark;
:el.style.borderLeft = '3px solid #10B981' (try/catch);
kill
}
fork again
' -------- _cleanupExpired() --------
partition "_cleanupExpired()" #FFF2E7 {
:_cleanupExpired;
:ttl = 30d; now = Date.now();\nfor (k,ts in cache) if !ts || now - ts > ttl -> delete;\nif dirty -> _save();
kill
}
fork again
' -------- clear() --------
partition "clear()" #E7F7FF {
:clear;
:cache = {}; _save();
kill
}
fork again
' -------- bootstrap (global) --------
partition "bootstrap (global)" #FFF7E7 {
:window.AI_REPO_HISTORY = new ConversationHistory();
kill
}
end fork
@enduml
' ==== METHOD: ConversationHistory.constructor() ============================
@startuml
title storage:constructor(): \n Build per-conversation key, load cache, prune expired
participant "Caller" as CL
participant "constructor()" as CTOR
participant "_getConversationId()" as GID
participant "_load()" as LOAD
participant "_cleanupExpired()" as CLEAN
activate CL
CL -> CTOR : new ConversationHistory()
activate CTOR
CTOR -> GID : _getConversationId()
GID --> CTOR : conversationId
CTOR -> CTOR : key = `ai_rc:conv:${conversationId}:processed`
CTOR -> LOAD : _load()
LOAD --> CTOR : cache object
CTOR -> CLEAN : _cleanupExpired()
CLEAN --> CTOR : done
CTOR --> CL : instance
deactivate CTOR
deactivate CL
@enduml
' ==== METHOD: _getConversationId() ========================================
@startuml
title storage:_getConversationId(): \n host+path key (normalize chat.openai.com → chatgpt.com)
participant "Caller" as CL
participant "_getConversationId()" as GID
participant "Location" as LOC
activate CL
CL -> GID : initial request
activate GID
GID -> LOC : read location.hostname, location.pathname
LOC --> GID : hostname, pathname
GID -> GID : host = hostname.replace('chat.openai.com','chatgpt.com')
GID --> CL : `${host}:${pathname || '/'}`
deactivate GID
deactivate CL
@enduml
' ==== METHOD: _load() / _save() ===========================================
@startuml
title storage:_load() / _save(): \n Safely read/write JSON cache in localStorage
participant "Caller" as CL
participant "_load()" as LOAD
participant "_save()" as SAVE
participant "localStorage" as LS
participant "Logger" as LOG
' _load()
activate CL
CL -> LOAD : initial request
activate LOAD
LOAD -> LS : getItem(key)
LS --> LOAD : json | null
LOAD -> LOAD : JSON.parse(json||'{}') catch -> {}
LOAD --> CL : cache object
deactivate LOAD
' _save()
CL -> SAVE : cache
activate SAVE
SAVE -> LS : setItem(key, JSON.stringify(cache))
alt success
LS --> SAVE : ok
SAVE --> CL : done
else error
LS --> SAVE : throw e
SAVE -> LOG : warn('Failed to save history cache', {error:e.message})
LOG --> SAVE : logged
SAVE --> CL : done
end
deactivate SAVE
deactivate CL
@enduml
' ==== METHOD: isProcessed(el, idx=0) ======================================
@startuml
title storage:isProcessed(el, idx): \n Check cache using fingerprint+index key
participant "Caller" as CL
participant "isProcessed(el,idx)" as ISP
participant "_fingerprint(el,idx)" as FP
activate CL
CL -> ISP : el, idx
activate ISP
ISP -> FP : _fingerprint(el, idx)
FP --> ISP : fp
ISP -> ISP : return Object.prototype.hasOwnProperty.call(cache, fp)
ISP --> CL : true/false
deactivate ISP
deactivate CL
@enduml
' ==== METHOD: markProcessed(el, idx=0) =====================================
@startuml
title storage:markProcessed(el, idx): \n Record timestamp, persist, optionally mark DOM
participant "Caller" as CL
participant "markProcessed(el,idx)" as MK
participant "_fingerprint(el,idx)" as FP
participant "_save()" as SAVE
participant "_mark(el)" as MRK
participant "Config" as CFG
activate CL
CL -> MK : el, idx
activate MK
MK -> FP : _fingerprint(el, idx)
FP --> MK : fp
MK -> MK : cache[fp] = Date.now()
MK -> SAVE : _save()
SAVE --> MK : ok
MK -> CFG : get('ui.showExecutedMarker')
CFG --> MK : bool
alt true
MK -> MRK : _mark(el)
MRK --> MK : done
end
MK --> CL : (void)
deactivate MK
deactivate CL
@enduml
' ==== METHOD: _fingerprint(el, idx) ========================================
@startuml
title storage:_fingerprint(el, idx): \n Use stable FP if available; else compat; else inline hash
participant "Caller" as CL
participant "_fingerprint(el,idx)" as FP
participant "AI_REPO_STABLE_FINGERPRINT" as STAB
participant "AI_REPO_FINGERPRINT" as COMP
participant "_hash(str)" as HSH
activate CL
CL -> FP : el, idx
activate FP
FP -> STAB : exists?
STAB --> FP : yes/no
alt stable available
FP -> STAB : STABLE_FINGERPRINT(el)
STAB --> FP : base
else no stable
FP -> COMP : exists?
COMP --> FP : yes/no
alt compat available
FP -> COMP : FINGERPRINT(el)
COMP --> FP : base
else none
FP -> FP : text = (el.textContent||'').slice(0,1000)
FP -> HSH : _hash(text)
HSH --> FP : base
end
end
FP --> CL : `${base}|idx:${idx}`
deactivate FP
deactivate CL
@enduml
' ==== METHOD: _hash(str) ===================================================
@startuml
title storage:_hash(str): \n djb2-xor over up to 1000 chars → base36
participant "Caller" as CL
participant "_hash(str)" as HSH
activate CL
CL -> HSH : str
activate HSH
HSH -> HSH : h=5381; for i<min(len,1000): h=((h<<5)+h)^charCode
HSH --> CL : (h>>>0).toString(36)
deactivate HSH
deactivate CL
@enduml
' ==== METHOD: _mark(el) ====================================================
@startuml
title storage:_mark(el): \n Visual left border to indicate executed command
participant "Caller" as CL
participant "_mark(el)" as MRK
participant "DOM" as DOM
activate CL
CL -> MRK : el
activate MRK
MRK -> DOM : el.style.borderLeft = '3px solid #10B981' (try/catch)
DOM --> MRK : ok | ignored
MRK --> CL : (void)
deactivate MRK
deactivate CL
@enduml
' ==== METHOD: _cleanupExpired() ============================================
@startuml
title storage:_cleanupExpired(): \n TTL pruning (30 days) + conditional save
participant "Caller" as CL
participant "_cleanupExpired()" as CLN
participant "_save()" as SAVE
activate CL
CL -> CLN : initial request
activate CLN
CLN -> CLN : ttl=30*24*60*60*1000; now=Date.now(); dirty=false
loop for each [k, ts] in Object.entries(cache)
alt !ts or now - ts > ttl
CLN -> CLN : delete cache[k]; dirty=true
end
end
alt dirty
CLN -> SAVE : _save()
SAVE --> CLN : ok
end
CLN --> CL : (void)
deactivate CLN
deactivate CL
@enduml
' ==== METHOD: clear() ======================================================
@startuml
title storage:clear(): \n Reset cache and persist
participant "Caller" as CL
participant "clear()" as CLR
participant "_save()" as SAVE
activate CL
CL -> CLR : initial request
activate CLR
CLR -> CLR : cache = {}
CLR -> SAVE : _save()
SAVE --> CLR : ok
CLR --> CL : (void)
deactivate CLR
deactivate CL
@enduml
' ==== METHOD: bootstrap (global) ===========================================
@startuml
title storage:bootstrap: \n Expose ConversationHistory singleton on window
participant "Caller" as CL
participant "bootstrap" as BOOT
participant "ConversationHistory()" as CTOR
participant "window" as WIN
activate CL
CL -> BOOT : module load
activate BOOT
BOOT -> CTOR : new ConversationHistory()
CTOR --> BOOT : instance
BOOT -> WIN : window.AI_REPO_HISTORY = instance
WIN --> BOOT : ok
BOOT --> CL : (void)
deactivate BOOT
deactivate CL
@enduml
' ==== LEGEND ===============================================================
@startuml
legend bottom
== storage 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 class lifeline; the module/class name appears in the title only.
3) Include every directly-called helper/system as a participant
(e.g., "_fingerprint()", "_hash()", "_mark()", "Config", "Logger", "localStorage", "Location").
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., 30-day TTL, base36 hashes, idx suffix, key prefix `ai_rc:conv:`).
• Color palette (soft pastels)
• Use --> for returns; -> for calls.
• Participants use quoted method names for internals; plain nouns for systems ("localStorage", "Location", "window").
• Keep this legend at the end of the file to standardize edits.
endlegend
@enduml

View File

@ -12,12 +12,26 @@
const cfg = () => window.AI_REPO_CONFIG;
const log = () => window.AI_REPO_LOGGER;
// Regex explanation for extractAllBlocks:
// /^\s*@bridge@[ \t]*\n([\s\S]*?)\n@end@[ \t]*(?:\n|$)/gm
// - ^\s* → allow optional leading whitespace at the start of a line (multiline mode)
// - @bridge@ → literal header marking the beginning of a command block
// - [ \t]*\n → optional spaces/tabs after the header, then a required newline
// - ([\s\S]*?) → CAPTURE non-greedily any characters (including newlines) as the block body
// - \n@end@ → require a newline followed by the literal terminator @end@
// - [ \t]* → allow trailing spaces/tabs after @end@
// - (?:\n|$) → require either a trailing newline or end-of-string (so blocks end cleanly)
// Flags:
// g = global (find all blocks in the text)
// m = multiline (^ and $ match line boundaries, not just the entire string)
// Returns an array of any block in the text that match the above description.
function extractAllBlocks(text) {
const out = []; const re = /^\s*@bridge@[ \t]*\n([\s\S]*?)\n@end@[ \t]*(?:\n|$)/gm;
let m; while ((m = re.exec(text)) !== null) out.push(m[0]);
return out;
}
// Returns true if the element contains any of these selectors
function isAssistantMsg(el) {
const selectors = [
'[data-message-author-role="assistant"]',
@ -27,6 +41,9 @@
return selectors.some(s => el.matches?.(s) || el.querySelector?.(s));
}
// Checks the content of detected command blocks in the element. Waits for a continuous window of
// (windowMs) time with no change. If all blocks are stable returns a string concatenation of all
// the blocks. If no blocks were ever found, it returns the original initial text.
async function settleText(el, initial, windowMs, pollMs) {
let deadline = Date.now() + windowMs;
let last = initial;
@ -56,6 +73,8 @@
log().info('[Detector] Starting advanced detector with settle/debounce');
// Observes DOM mutations to detect new/updated assistant messages; on new assistant nodes it
// enqueues handling, and logs when text changes.
this.observer = new MutationObserver((mutations) => {
if (cfg().get('runtime.paused')) {
log().trace('[Detector] Mutations ignored (paused)');
@ -85,6 +104,7 @@
}
});
// Tell the observer to watch the entire document body.
this.observer.observe(document.body, { childList: true, subtree: true, characterData: true, attributes: true });
log().verbose('[Detector] MutationObserver attached (childList, subtree, characterData, attributes)');