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