399 lines
11 KiB
Plaintext
399 lines
11 KiB
Plaintext
' ===================================================================
|
|
' 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
|