added comments
This commit is contained in:
parent
c408289e0f
commit
3fcfb6b6e0
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)');
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue