AI-Repo-Commander/Docs/diagrams/file_diagrams/main.puml

606 lines
18 KiB
Plaintext

' ===================================================================
' 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