474 lines
14 KiB
Plaintext
474 lines
14 KiB
Plaintext
' ===================================================================
|
|
' 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
|