added comments
This commit is contained in:
parent
b3a6986dc5
commit
c408289e0f
|
|
@ -0,0 +1,90 @@
|
|||
@startuml
|
||||
title AI Repo Commander — Config overview (safe)
|
||||
|
||||
class ConfigManager {
|
||||
+load() : object
|
||||
+save() : void
|
||||
+get(keyPath) : any
|
||||
+set(keyPath, value) : void
|
||||
+mergeConfigs(defaults, saved) : object
|
||||
+deepClone(o) : any
|
||||
}
|
||||
|
||||
class STORAGE_KEYS {
|
||||
history : "ai_repo_commander_executed"
|
||||
cfg : "ai_repo_commander_cfg"
|
||||
panel : "ai_repo_commander_panel_state"
|
||||
}
|
||||
|
||||
' Split DEFAULT_CONFIG into simple classes (no quotes, no =)
|
||||
class DEFAULT_META {
|
||||
version : 1.6.2
|
||||
}
|
||||
class DEFAULT_API {
|
||||
enabled : boolean
|
||||
timeout : number
|
||||
maxRetries : number
|
||||
bridgeKey : string
|
||||
}
|
||||
class DEFAULT_DEBUG {
|
||||
enabled : boolean
|
||||
level : number
|
||||
maxLines : number
|
||||
showPanel : boolean
|
||||
}
|
||||
class DEFAULT_EXECUTION {
|
||||
debounceDelay : ms
|
||||
settleCheckMs : ms
|
||||
settlePollMs : ms
|
||||
requireTerminator : boolean
|
||||
coldStartMs : ms
|
||||
stuckAfterMs : ms
|
||||
scanDebounceMs : ms
|
||||
fastWarnMs : ms
|
||||
slowWarnMs : ms
|
||||
clusterRescanMs : ms
|
||||
clusterMaxLookahead : count
|
||||
}
|
||||
class DEFAULT_QUEUE {
|
||||
minDelayMs : ms
|
||||
maxPerMinute : count
|
||||
maxPerMessage : count
|
||||
waitForComposerMs : ms
|
||||
}
|
||||
class DEFAULT_UI {
|
||||
autoSubmit : boolean
|
||||
appendTrailingNewline : boolean
|
||||
postPasteDelayMs : ms
|
||||
showExecutedMarker : boolean
|
||||
processExisting : boolean
|
||||
submitMode : "enter|ctrl+enter"
|
||||
maxComposerWaitMs : ms
|
||||
submitMaxRetries : count
|
||||
}
|
||||
class DEFAULT_STORAGE {
|
||||
dedupeTtlMs : ms
|
||||
cleanupAfterMs : ms
|
||||
cleanupIntervalMs : ms
|
||||
}
|
||||
class DEFAULT_RESPONSE {
|
||||
bufferFlushDelayMs : ms
|
||||
sectionHeadings : boolean
|
||||
maxPasteChars : number
|
||||
splitLongResponses : boolean
|
||||
}
|
||||
class DEFAULT_RUNTIME {
|
||||
paused : boolean
|
||||
'(note: runtime is not persisted)
|
||||
}
|
||||
|
||||
ConfigManager --> STORAGE_KEYS : uses localStorage keys
|
||||
ConfigManager --> DEFAULT_META : deepClone/merge
|
||||
ConfigManager --> DEFAULT_API
|
||||
ConfigManager --> DEFAULT_DEBUG
|
||||
ConfigManager --> DEFAULT_EXECUTION
|
||||
ConfigManager --> DEFAULT_QUEUE
|
||||
ConfigManager --> DEFAULT_UI
|
||||
ConfigManager --> DEFAULT_STORAGE
|
||||
ConfigManager --> DEFAULT_RESPONSE
|
||||
ConfigManager --> DEFAULT_RUNTIME : in-memory only
|
||||
@enduml
|
||||
|
|
@ -0,0 +1,445 @@
|
|||
' ===================================================================
|
||||
' File: ConfigManager.puml
|
||||
' Purpose: Single source of truth for class-level activity and per-method sequences.
|
||||
' Edit rules: Follow the legend at bottom; preserve VIEW/METHOD anchors for automation.
|
||||
' ===================================================================
|
||||
|
||||
' (Optional) neutral defaults — typography/layout only (keeps your colors intact)
|
||||
skinparam Shadowing false
|
||||
skinparam SequenceMessageAlign center
|
||||
skinparam SequenceLifeLineBorderColor #666666
|
||||
skinparam SequenceLifeLineBorderThickness 1
|
||||
|
||||
' ==== VIEW: Branch Flow (full class) ==========================================
|
||||
@startuml
|
||||
title ConfigManager — Branch Flow (full class)
|
||||
|
||||
start
|
||||
:ConfigManager;
|
||||
|
||||
' Fan-out to each method
|
||||
fork
|
||||
' -------- constructor() --------
|
||||
partition "constructor()" #E7FAE3 {
|
||||
:constructor();
|
||||
:this.config = load();
|
||||
kill
|
||||
}
|
||||
fork again
|
||||
' -------- load() --------
|
||||
partition "load()" #FFF6D1 {
|
||||
:load();
|
||||
:raw = localStorage.getItem(STORAGE_KEYS.cfg);
|
||||
if (raw is null/empty?) then (yes)
|
||||
:config = deepClone(DEFAULT_CONFIG);
|
||||
kill
|
||||
else (no)
|
||||
:try parse = JSON.parse(raw);
|
||||
if (parse ok?) then (yes)
|
||||
:saved = parse;
|
||||
:config = mergeConfigs(DEFAULT_CONFIG, saved);
|
||||
kill
|
||||
else (no / parse error)
|
||||
:config = deepClone(DEFAULT_CONFIG);
|
||||
kill
|
||||
endif
|
||||
endif
|
||||
}
|
||||
fork again
|
||||
' -------- save() --------
|
||||
partition "save()" #FFE1DB {
|
||||
:save();
|
||||
:persistable = deepClone(config);
|
||||
if (persistable has runtime?) then (yes)
|
||||
:delete persistable.runtime;
|
||||
endif
|
||||
:try json = JSON.stringify(persistable);
|
||||
if (stringify ok?) then (yes)
|
||||
:localStorage.setItem(STORAGE_KEYS.cfg, json);
|
||||
kill
|
||||
else (no / stringify error)
|
||||
:/* log/notify failure */;
|
||||
kill
|
||||
endif
|
||||
}
|
||||
fork again
|
||||
' -------- get(keyPath) --------
|
||||
partition "get(keyPath)" #DCF9EE {
|
||||
:get(keyPath);
|
||||
:parts = keyPath.split('.');
|
||||
:node = config;
|
||||
while (more parts?)
|
||||
:p = next part;
|
||||
if (node[p] exists?) then (yes)
|
||||
:node = node[p];
|
||||
else (no)
|
||||
:return undefined;
|
||||
kill
|
||||
endif
|
||||
endwhile
|
||||
:return node;
|
||||
kill
|
||||
}
|
||||
fork again
|
||||
' -------- set(keyPath, value) --------
|
||||
partition "set(keyPath, value)" #FFE6F0 {
|
||||
:set(keyPath, value);
|
||||
:parts = keyPath.split('.');
|
||||
:node = config;
|
||||
while (more parts?)
|
||||
:p = next part;
|
||||
if (node[p] exists?) then (yes)
|
||||
:node = node[p];
|
||||
else (no)
|
||||
:node[p] = {};
|
||||
:node = node[p];
|
||||
endif
|
||||
endwhile
|
||||
:assign value at final key;
|
||||
:save();
|
||||
kill
|
||||
}
|
||||
fork again
|
||||
' -------- mergeConfigs(defaults, saved) --------
|
||||
partition "mergeConfigs(defaults, saved)" #E6F3FF {
|
||||
:mergeConfigs(defaults, saved);
|
||||
:result = deepClone(defaults);
|
||||
if (saved is object?) then (yes)
|
||||
:for each key k in saved;
|
||||
while (keys left?)
|
||||
:k = next key;
|
||||
if (k == "runtime"?) then (yes)
|
||||
:skip;
|
||||
else (no)
|
||||
if (both result[k] and saved[k] are plain objects?) then (yes)
|
||||
:result[k] = mergeConfigs(result[k], saved[k]);
|
||||
else (no)
|
||||
:result[k] = deepClone(saved[k]);
|
||||
endif
|
||||
endif
|
||||
endwhile
|
||||
else (no)
|
||||
:/* nothing to merge */;
|
||||
endif
|
||||
:return result;
|
||||
kill
|
||||
}
|
||||
fork again
|
||||
' -------- deepClone(o) --------
|
||||
partition "deepClone(o)" #F0E6FA {
|
||||
:deepClone(o);
|
||||
if (o is null or primitive?) then (yes)
|
||||
:return o;
|
||||
kill
|
||||
else (no)
|
||||
if (o is Array?) then (yes)
|
||||
:clone = [];
|
||||
:for each item -> push( deepClone(item) );
|
||||
:return clone;
|
||||
kill
|
||||
else (no)
|
||||
:clone = {};
|
||||
:for each key -> clone[key] = deepClone(o[key]);
|
||||
:return clone;
|
||||
kill
|
||||
endif
|
||||
endif
|
||||
}
|
||||
end fork
|
||||
@enduml
|
||||
|
||||
' ==== METHOD: constructor() ================================================
|
||||
@startuml
|
||||
title ConfigManager:constructor(): \n Populate this.config at instantiation
|
||||
|
||||
actor Page as PG
|
||||
participant "constructor()" as CTOR
|
||||
participant "load()" as LD
|
||||
|
||||
PG -> CTOR : new ConfigManager()
|
||||
activate CTOR
|
||||
CTOR -> LD : populate this.config
|
||||
LD --> CTOR : config object
|
||||
CTOR --> PG : this.config set
|
||||
deactivate CTOR
|
||||
@enduml
|
||||
|
||||
' ==== METHOD: load() =======================================================
|
||||
@startuml
|
||||
title ConfigManager:load(): \n Read from localStorage, parse+merge or fallback to defaults
|
||||
|
||||
participant "Caller" as CL
|
||||
participant "load()" as LD
|
||||
participant "localStorage" as LS
|
||||
participant "mergeConfigs()" as MC
|
||||
participant "deepClone()" as DC
|
||||
|
||||
activate CL
|
||||
activate LD
|
||||
CL -> LD : initial request
|
||||
activate LS
|
||||
LD -> LS : getItem(STORAGE_KEYS.cfg)
|
||||
LS --> LD : getItem(STORAGE_KEYS.cfg)
|
||||
deactivate LS
|
||||
alt STORAGE_KEYS.cfg (Empty)
|
||||
activate DC
|
||||
LD -> DC : deepClone(DEFAULT_CONFIG)
|
||||
LD <-- DC : defaults clone
|
||||
deactivate DC
|
||||
LD --> CL : return defaults
|
||||
else STORAGE_KEYS.cfg (Not empty)
|
||||
LD --> LD : try parse STORAGE_KEYS.cfg
|
||||
alt parse ok
|
||||
LD --> LD : saved = parsed
|
||||
activate MC
|
||||
LD -> MC : mergeConfigs(DEFAULT_CONFIG, saved)
|
||||
MC --> LD : merged config
|
||||
deactivate MC
|
||||
LD --> CL : return merged config
|
||||
else parse error
|
||||
activate DC
|
||||
LD -> DC : deepClone(DEFAULT_CONFIG)
|
||||
LD <-- DC : defaults clone
|
||||
deactivate DC
|
||||
LD --> CL : return defaults
|
||||
end
|
||||
end
|
||||
@enduml
|
||||
|
||||
' ==== METHOD: save() =======================================================
|
||||
@startuml
|
||||
title ConfigManager:save(): \n Strip runtime, stringify, persist to localStorage
|
||||
|
||||
participant "Caller" as CL
|
||||
participant "save()" as SV
|
||||
participant "deepClone()" as DC
|
||||
participant "JSON" as JS
|
||||
participant "localStorage" as LS
|
||||
participant "Console" as CLG
|
||||
|
||||
activate CL
|
||||
CL -> SV : initial request
|
||||
deactivate CL
|
||||
activate SV
|
||||
activate DC
|
||||
SV -> DC : deepClone(config)
|
||||
DC --> SV : persistable clone
|
||||
deactivate DC
|
||||
|
||||
SV -> SV : delete any persistable.runtime
|
||||
activate JS
|
||||
SV -> JS : JSON.stringify(persistable)
|
||||
alt stringify ok
|
||||
JS --> SV : json string
|
||||
activate LS
|
||||
SV -> LS : setItem(STORAGE_KEYS.cfg, json)
|
||||
deactivate LS
|
||||
else stringify error
|
||||
JS --> SV : error
|
||||
activate CLG
|
||||
SV -> CLG : log/notify failure
|
||||
deactivate CLG
|
||||
end
|
||||
deactivate JS
|
||||
@enduml
|
||||
|
||||
' ==== METHOD: get(keyPath) ================================================
|
||||
@startuml
|
||||
title ConfigManager:get(keyPath): \n Resolve a dotted path or return undefined
|
||||
|
||||
participant "Caller" as CL
|
||||
participant "get(keyPath)" as GET
|
||||
|
||||
activate CL
|
||||
CL -> GET : initial request (keyPath)
|
||||
activate GET
|
||||
|
||||
GET -> GET : parts = keyPath.split('.'); node = config
|
||||
loop for each part
|
||||
alt node has part
|
||||
GET -> GET : node = node[part]
|
||||
else missing segment
|
||||
GET --> CL : undefined
|
||||
end
|
||||
end
|
||||
|
||||
GET --> CL : value (final node)
|
||||
deactivate GET
|
||||
deactivate CL
|
||||
@enduml
|
||||
|
||||
' ==== METHOD: set(keyPath, value) =========================================
|
||||
@startuml
|
||||
title ConfigManager:set(keyPath, value): \n Create missing path segments, assign, then persist
|
||||
|
||||
participant "Caller" as CL
|
||||
participant "set(keyPath, value)" as SET
|
||||
participant "save()" as SV
|
||||
|
||||
activate CL
|
||||
CL -> SET : initial request (keyPath, value)
|
||||
activate SET
|
||||
|
||||
SET -> SET : parts = keyPath.split('.'); node = config
|
||||
loop for each part (except last)
|
||||
alt node has part
|
||||
SET -> SET : node = node[part]
|
||||
else missing
|
||||
SET -> SET : node[part] = {}; node = node[part]
|
||||
end
|
||||
end
|
||||
|
||||
SET -> SET : assign value at final key
|
||||
SET -> SV : save()
|
||||
SV --> SET : persisted
|
||||
|
||||
SET --> CL : done
|
||||
deactivate SET
|
||||
deactivate CL
|
||||
@enduml
|
||||
|
||||
' ==== METHOD: mergeConfigs(defaults, saved) ================================
|
||||
@startuml
|
||||
title ConfigManager:mergeConfigs(defaults, saved): \n Deep merge saved over defaults (skip runtime)
|
||||
|
||||
participant "Caller" as CL
|
||||
participant "mergeConfigs()" as MC
|
||||
participant "deepClone()" as DC
|
||||
|
||||
activate CL
|
||||
CL -> MC : initial request (defaults, saved)
|
||||
activate MC
|
||||
|
||||
MC -> DC : deepClone(defaults)
|
||||
DC --> MC : result (clone of defaults)
|
||||
|
||||
alt saved is plain object
|
||||
loop for each key k in saved
|
||||
alt k == "runtime"
|
||||
MC -> MC : skip key
|
||||
else not runtime
|
||||
alt both result[k] and saved[k] are plain objects
|
||||
MC -> MC : result[k] = mergeConfigs(result[k], saved[k]) ' recursive
|
||||
else overwrite
|
||||
MC -> DC : deepClone(saved[k])
|
||||
DC --> MC : cloned value
|
||||
MC -> MC : result[k] = cloned value
|
||||
end
|
||||
end
|
||||
end
|
||||
else saved not object
|
||||
MC -> MC : nothing to merge
|
||||
end
|
||||
|
||||
MC --> CL : result
|
||||
deactivate MC
|
||||
deactivate CL
|
||||
@enduml
|
||||
|
||||
' ==== METHOD: deepClone(o) ================================================
|
||||
@startuml
|
||||
title ConfigManager:deepClone(o): \n Structural clone for arrays/objects; primitives by value
|
||||
|
||||
participant "Caller" as CL
|
||||
participant "deepClone()" as DC
|
||||
|
||||
activate CL
|
||||
CL -> DC : initial request (o)
|
||||
activate DC
|
||||
|
||||
alt o is null or primitive
|
||||
DC --> CL : o
|
||||
deactivate DC
|
||||
deactivate CL
|
||||
return
|
||||
else non-primitive
|
||||
alt Array
|
||||
DC -> DC : clone = []
|
||||
loop each item
|
||||
DC -> DC : clone.push( deepClone(item) )
|
||||
end
|
||||
DC --> CL : clone
|
||||
else Object
|
||||
DC -> DC : clone = {}
|
||||
loop each key
|
||||
DC -> DC : clone[key] = deepClone(o[key])
|
||||
end
|
||||
DC --> CL : clone
|
||||
end
|
||||
end
|
||||
|
||||
deactivate DC
|
||||
deactivate CL
|
||||
@enduml
|
||||
|
||||
' ==== LEGEND ===============================================================
|
||||
@startuml
|
||||
legend bottom
|
||||
== Config UML Style Guide (for future edits) ==
|
||||
• Scope: One .puml per class or file. Keep two views:
|
||||
(1) Activity "Branch Flow" for all methods for the class or functions in the file.
|
||||
- (partitions + soft colors),
|
||||
(2) Per-method Sequence diagrams for each of the methods or functions.
|
||||
|
||||
• Sequence conventions:
|
||||
1) First participant is the external caller (use "Caller" or "Page").
|
||||
2) Do NOT add the class lifeline unless needed (constructor). Class name appears in title only.
|
||||
3) Include every directly-called method or subsystem as a participant
|
||||
(e.g., "load()", "mergeConfigs()", "deepClone()", "JSON", "localStorage", "Console").
|
||||
4) Prefer simple messages.
|
||||
5) Use activate/deactivate for the method under focus and key collaborators.
|
||||
6) Use alt blocks only when branches meaningfully change the message flow.
|
||||
For trivial checks (e.g., delete runtime if exists), inline the action.
|
||||
7) Titles: "ClassName:method(): \n Detailed description of the flow".
|
||||
|
||||
• Activity view conventions:
|
||||
A) Start with Class(or filename) node then fork partitions for each method or function.
|
||||
B) One partition per method; soft background color; terminate branches with 'kill'.
|
||||
C) Keep wording aligned with code (e.g., "deepClone(DEFAULT_CONFIG)", "mergeConfigs(...)").
|
||||
|
||||
• Color palette (soft pastels)
|
||||
• Use --> for returns; -> for calls.
|
||||
• Participants use quoted method names for internals (e.g., "save()"), and plain nouns for systems ("JSON", "localStorage", "Console").
|
||||
• When modifying this file keep this legend at the end of the file to standardize edits.
|
||||
|
||||
UML_Example
|
||||
------------------------------------------
|
||||
title ClassName:methodName(args): \n Detailed description of what this method does
|
||||
|
||||
participant "Caller" as CL
|
||||
participant "methodName()" as M ' the method under focus
|
||||
' Add collaborators as needed:
|
||||
' participant "SomeDependency" as DEP
|
||||
' participant "AnotherMethod()" as AM
|
||||
' participant "JSON" as JS
|
||||
' participant "localStorage" as LS
|
||||
|
||||
activate CL
|
||||
CL -> M : initial request (args)
|
||||
activate M
|
||||
' -- inner flow (keep alt blocks only if they clarify) --
|
||||
' Example pattern:
|
||||
activate LS
|
||||
' M -> LS : getItem(KEY)
|
||||
' LS --> M : value
|
||||
deactivate LS
|
||||
|
||||
' alt branch condition
|
||||
activate AM
|
||||
' M -> AM : call anotherMethod(...)
|
||||
' AM --> M : result
|
||||
deactivate AM
|
||||
' else other branch
|
||||
activate DEP
|
||||
' M -> DEP : do something
|
||||
' DEP -> M : ok
|
||||
deactivate DEP
|
||||
' end
|
||||
|
||||
' Return to caller
|
||||
M -> CL : return value
|
||||
deactivate M
|
||||
deactivate CL
|
||||
---------------------------------------------
|
||||
endlegend
|
||||
@enduml
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
|
||||
@startuml Command Processing State Machine
|
||||
skinparam shadowing false
|
||||
skinparam ArrowColor #555
|
||||
skinparam state {
|
||||
StartColor #A5D6A7
|
||||
BackgroundColor #FAFAFA
|
||||
}
|
||||
|
||||
[*] --> DETECTED : YAML block found
|
||||
|
||||
state DETECTED {
|
||||
}
|
||||
DETECTED --> PARSING : debounce/settle passed
|
||||
PARSING --> VALIDATING : YAML parsed
|
||||
PARSING --> ERROR : parse failure
|
||||
|
||||
VALIDATING --> DEDUPE_CHECK : required fields ok
|
||||
VALIDATING --> ERROR : validation failed
|
||||
|
||||
state DEDUPE_CHECK
|
||||
DEDUPE_CHECK --> SKIPPED : duplicate or example:true
|
||||
DEDUPE_CHECK --> READY : new and runnable
|
||||
|
||||
state READY
|
||||
READY --> EXECUTING : user intent or auto-exec policy
|
||||
READY --> [*] : STOP triggered
|
||||
|
||||
state EXECUTING
|
||||
EXECUTING --> COMPLETE : success
|
||||
EXECUTING --> ERROR : API/network failure
|
||||
|
||||
state SKIPPED
|
||||
SKIPPED --> [*]
|
||||
|
||||
state COMPLETE
|
||||
COMPLETE --> [*]
|
||||
|
||||
state ERROR
|
||||
ERROR --> [*]
|
||||
|
||||
@enduml
|
||||
|
|
@ -1,4 +1,9 @@
|
|||
// ==COMMAND EXECUTOR START==
|
||||
// Module: command-executor.js
|
||||
// Depends on: config.js, logger.js
|
||||
// Purpose: Execute validated repo commands via the bridge API (GM_xmlhttpRequest),
|
||||
// handling retries/timeouts and rendering success/error effects. Stores large
|
||||
// response bodies for paste-submit.
|
||||
/* global GM_xmlhttpRequest */
|
||||
/* global GM_notification */
|
||||
(function () {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,20 @@
|
|||
// ==COMMAND PARSER START==
|
||||
// Module: command-parser.js
|
||||
// Purpose: Extract and parse YAML-like command blocks embedded in assistant messages.
|
||||
// - Looks for a complete block delimited by @bridge@ ... @end@
|
||||
// - Parses simple key/value pairs and multiline "|" blocks
|
||||
// - Applies sane defaults (url, owner, source_branch)
|
||||
// - Validates presence of required fields per action
|
||||
// This module is side-effect free and exposes a single class via window.AI_REPO_PARSER.
|
||||
(function () {
|
||||
/**
|
||||
* CommandParser
|
||||
* - parse(text): Extracts first complete command block and returns a structured object
|
||||
* - extractBlock(text): Returns the inner text between @bridge@ and @end@
|
||||
* - parseKV(block): Minimal YAML-like parser supporting multi-line values with "|"
|
||||
* - applyDefaults(obj): Applies default values and owner/repo split logic
|
||||
* - validate(obj): Returns { isValid, errors, example? }
|
||||
*/
|
||||
class CommandParser {
|
||||
static REQUIRED = {
|
||||
get_file: ['action', 'repo', 'path'],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
// ==DEBUG PANEL START==
|
||||
// Module: debug-panel.js
|
||||
// Depends on: config.js, logger.js, queue.js, storage.js
|
||||
// Purpose: In-page draggable panel showing recent logs and exposing tools/settings.
|
||||
// - Logs tab: tail of the Logger buffer with copy buttons
|
||||
// - Tools & Settings: toggles and numeric inputs bound to config, quick actions
|
||||
// - Pause/Stop controls and queue size indicator
|
||||
// The panel stores its position/collapsed state in localStorage (see config.js STORAGE_KEYS).
|
||||
/* global GM_notification */
|
||||
(function () {
|
||||
const cfg = () => window.AI_REPO_CONFIG;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
// ==DETECTOR START==
|
||||
// Module: detector.js
|
||||
// Depends on: config.js, logger.js, queue.js, command-parser.js, command-executor.js, storage.js
|
||||
// Purpose: Observe assistant messages in the DOM, wait for streaming to finish,
|
||||
// extract @bridge@...@end@ command blocks, and enqueue them for execution.
|
||||
// Key features:
|
||||
// - Debounce window to allow AI streaming to complete
|
||||
// - Settle check: ensures text remains stable for a small window
|
||||
// - Per-message command cap and de-duplication via storage.js
|
||||
// - Optional processing of existing messages on page load
|
||||
(function () {
|
||||
const cfg = () => window.AI_REPO_CONFIG;
|
||||
const log = () => window.AI_REPO_LOGGER;
|
||||
|
|
|
|||
|
|
@ -1,23 +1,72 @@
|
|||
// ==FINGERPRINT (drop-in utility) ==
|
||||
// Purpose: Generate a strong, stable fingerprint string for an assistant message element.
|
||||
//
|
||||
// This utility is designed to uniquely identify assistant messages across dynamic UIs
|
||||
// (ChatGPT, Claude, Gemini). It uses multiple content- and position-aware components:
|
||||
// - ch: content hash of the most "command-like" text in the element (prefers fenced code)
|
||||
// - ph: hash of the immediately preceding assistant messages' trailing text (context)
|
||||
// - ih: hash of the text right before the first @bridge@...@end@ block (intra-message prefix)
|
||||
// - hint: small DOM hint (tag#id.class) hashed to keep size small
|
||||
// - n: ordinal among elements with same ch+ph+ih on the page at this moment
|
||||
//
|
||||
// The combination helps disambiguate near-duplicate messages, re-ordered DOM, and small edits.
|
||||
// Fingerprints are stable enough to persist in localStorage for de-duplication.
|
||||
//
|
||||
// Notes:
|
||||
// - All hashes are short base36 strings derived from a djb2-xor style hash; fast and sufficient.
|
||||
// - Inputs are normalized (trim, strip zero-width spaces, normalize whitespace before newlines).
|
||||
// - The algorithm intentionally looks at at most ~2000 chars per slice for performance.
|
||||
// - The module is side-effect free except for optional caching of a stable fingerprint in dataset.
|
||||
(function(){
|
||||
/**
|
||||
* CSS selectors that identify assistant messages across supported sites.
|
||||
* These are joined with "," and used for querySelectorAll when scanning neighbors.
|
||||
*/
|
||||
const MSG_SELECTORS = [
|
||||
'[data-message-author-role="assistant"]',
|
||||
'.chat-message:not([data-message-author-role="user"])',
|
||||
'.message-content'
|
||||
];
|
||||
|
||||
/**
|
||||
* Normalize text by removing carriage returns and zero-width spaces, squashing trailing
|
||||
* whitespace before newlines, and trimming ends. Keeps a consistent basis for hashing.
|
||||
* @param {string} s
|
||||
* @returns {string}
|
||||
*/
|
||||
function norm(s){ return (s||'').replace(/\r/g,'').replace(/\u200b/g,'').replace(/[ \t]+\n/g,'\n').trim(); }
|
||||
|
||||
/**
|
||||
* Fast, low-collision string hash. djb2 variant using XOR; returns unsigned base36 string.
|
||||
* @param {string} s
|
||||
* @returns {string}
|
||||
*/
|
||||
function hash(s){ let h=5381; for(let i=0;i<s.length;i++) h=((h<<5)+h)^s.charCodeAt(i); return (h>>>0).toString(36); }
|
||||
|
||||
/**
|
||||
* Extract the most relevant, command-like text from a message element.
|
||||
* Preference order:
|
||||
* - Any code/pre blocks that appear to contain a valid @bridge@ ... @end@ command with `action:`
|
||||
* - Otherwise, fall back to the element's textContent (first 2000 chars), normalized.
|
||||
* @param {Element} el
|
||||
* @returns {string}
|
||||
*/
|
||||
function commandLikeText(el){
|
||||
const blocks = el.querySelectorAll('pre code, pre, code');
|
||||
for (const b of blocks) {
|
||||
const t = norm(b.textContent || '');
|
||||
// Must look like a complete runnable command block
|
||||
if (/@end@\s*$/m.test(t) && /(^|\n)\s*@bridge@\b/m.test(t) && /(^|\n)\s*action\s*:/m.test(t)) return t;
|
||||
}
|
||||
return norm((el.textContent || '').slice(0, 2000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a hash of the immediate previous assistant messages' trailing text (up to 2000 chars).
|
||||
* This captures conversational context that helps distinguish repeated content.
|
||||
* @param {Element} el - The current message element
|
||||
* @returns {string} base36 hash of the context window
|
||||
*/
|
||||
function prevContextHash(el) {
|
||||
const list = Array.from(document.querySelectorAll(MSG_SELECTORS.join(',')));
|
||||
const idx = list.indexOf(el); if (idx <= 0) return '0';
|
||||
|
|
@ -29,6 +78,12 @@
|
|||
return hash(buf.slice(-2000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a hash of the text immediately preceding the first command block within this element.
|
||||
* If there is no @bridge@ block, hashes the trailing slice of the whole element text.
|
||||
* @param {Element} el
|
||||
* @returns {string}
|
||||
*/
|
||||
function intraPrefixHash(el){
|
||||
const t = el.textContent || '';
|
||||
const m = t.match(/@bridge@[\s\S]*?@end@/m);
|
||||
|
|
@ -36,6 +91,12 @@
|
|||
return hash(norm(t.slice(Math.max(0, endIdx - 2000), endIdx)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce a tiny DOM hint string (tag#id.class) to help separate identical content in different
|
||||
* containers. Limited to 40 chars and later hashed before inclusion in the final fingerprint.
|
||||
* @param {Element} node
|
||||
* @returns {string}
|
||||
*/
|
||||
function domHint(node) {
|
||||
if (!node) return '';
|
||||
const id = node.id || '';
|
||||
|
|
@ -43,6 +104,14 @@
|
|||
return `${node.tagName || ''}#${id}.${cls}`.slice(0, 40);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the ordinal index (0-based) of this element among all message elements that share
|
||||
* the same content/context key on the page. This disambiguates duplicates that have identical
|
||||
* ch+ph+ih values by adding their order of appearance.
|
||||
* @param {Element} el
|
||||
* @param {string} key - The key built from ch|ph|ih for this element
|
||||
* @returns {number}
|
||||
*/
|
||||
function ordinalForKey(el, key) {
|
||||
const list = Array.from(document.querySelectorAll(MSG_SELECTORS.join(',')));
|
||||
let n = 0;
|
||||
|
|
@ -61,6 +130,12 @@
|
|||
return n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a composite fingerprint for the given message element.
|
||||
* Format: "ch:<..>|ph:<..>|ih:<..>|hint:<..>|n:<ordinal>"
|
||||
* @param {Element} el
|
||||
* @returns {string}
|
||||
*/
|
||||
function fingerprintElement(el){
|
||||
const ch = hash(commandLikeText(el).slice(0, 2000));
|
||||
const ph = prevContextHash(el);
|
||||
|
|
@ -71,6 +146,12 @@
|
|||
return `${key}|hint:${dh}|n:${n}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve (and cache) a stable fingerprint for the element. The first time it's computed,
|
||||
* the value is stored in el.dataset.aiRcStableFp so subsequent calls don't recompute.
|
||||
* @param {Element} el
|
||||
* @returns {string}
|
||||
*/
|
||||
function getStableFingerprint(el) {
|
||||
if (el?.dataset?.aiRcStableFp) return el.dataset.aiRcStableFp;
|
||||
const fp = fingerprintElement(el);
|
||||
|
|
@ -78,7 +159,7 @@
|
|||
return fp;
|
||||
}
|
||||
|
||||
// Expose both for backward compatibility
|
||||
// Expose both for backward compatibility with older modules that expect these globals
|
||||
window.AI_REPO_FINGERPRINT = fingerprintElement;
|
||||
window.AI_REPO_STABLE_FINGERPRINT = getStableFingerprint;
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
// ==MAIN START==
|
||||
// Module: main.js
|
||||
// Purpose: Legacy entry point and convenience API exposure.
|
||||
// - Initializes observer and optionally scans existing messages
|
||||
// - Exposes window.AI_REPO with pause/resume/clearHistory helpers
|
||||
// Note: detector.js implements the primary monitoring pipeline; this module
|
||||
// remains for compatibility and console convenience.
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
// ==PASTE SUBMIT START==
|
||||
// Module: paste-submit.js
|
||||
// Depends on: config.js, logger.js
|
||||
// Purpose: Paste text into the site's chat composer (ChatGPT/Claude/Gemini styles)
|
||||
// and optionally auto-submit. Provides multiple robust strategies for editors.
|
||||
// Flow:
|
||||
// - findComposer(): locate the editable area
|
||||
// - pasteInto(el, text): try a series of paste strategies from cleanest to fallback
|
||||
// - findSendButton(scopeEl): locate a send/submit button near the editor
|
||||
// - submitToComposer(text): paste and, if enabled, click Send or press Enter
|
||||
/* global GM_setClipboard */
|
||||
(function () {
|
||||
const cfg = () => window.AI_REPO_CONFIG;
|
||||
|
|
|
|||
12
src/queue.js
12
src/queue.js
|
|
@ -1,6 +1,18 @@
|
|||
// ==QUEUE START==
|
||||
// Module: queue.js
|
||||
// Depends on: config.js, logger.js
|
||||
// Purpose: Rate-limited FIFO queue to serialize command execution.
|
||||
// - Enforces min delay between tasks (queue.minDelayMs)
|
||||
// - Caps tasks started per rolling minute (queue.maxPerMinute)
|
||||
// - Drains asynchronously; push is fire-and-forget
|
||||
(function () {
|
||||
/**
|
||||
* ExecutionQueue
|
||||
* push(task): enqueue an async function to run later
|
||||
* clear(): drop all pending tasks
|
||||
* size(): return queue length
|
||||
* Internals: _drain() runs tasks while respecting rate limits
|
||||
*/
|
||||
class ExecutionQueue {
|
||||
constructor(opts = {}) {
|
||||
const cfg = window.AI_REPO_CONFIG;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,18 @@
|
|||
// ==RESPONSE BUFFER START==
|
||||
// Module: response-buffer.js
|
||||
// Depends on: config.js, logger.js, queue.js, paste-submit.js
|
||||
// Purpose: Collect result chunks (e.g., from get_file/list_files) and paste them
|
||||
// into the chat composer in a controlled way.
|
||||
// - Buffers multiple sections briefly to batch them
|
||||
// - Splits very long payloads while respecting code fences
|
||||
// - Enqueues paste operations on the ExecutionQueue
|
||||
(function () {
|
||||
/**
|
||||
* Split a string by an approximate character limit, preferring to break at newline boundaries.
|
||||
* @param {string} s
|
||||
* @param {number} limit
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function chunkByLines(s, limit) {
|
||||
const out = []; let start = 0;
|
||||
while (start < s.length) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,15 @@
|
|||
// ==STORAGE START==
|
||||
// Module: storage.js
|
||||
// Purpose: Conversation-aware de-duplication of executed commands using localStorage.
|
||||
// - Creates a per-conversation key (host + path) to avoid cross-thread pollution
|
||||
// - Stores fingerprints of message elements (plus command index) with timestamps
|
||||
// - Provides isProcessed/markProcessed helpers and TTL cleanup
|
||||
(function () {
|
||||
/**
|
||||
* ConversationHistory
|
||||
* Tracks which commands (by message fingerprint + index) have been executed
|
||||
* in the current conversation. Uses a 30-day TTL for records.
|
||||
*/
|
||||
class ConversationHistory {
|
||||
constructor() {
|
||||
this.conversationId = this._getConversationId();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// ==UserScript==
|
||||
// Bootstrap loader: this userscript pulls in all modules via @require in dependency order.
|
||||
// @name AI Repo Commander (Full Features)
|
||||
// @namespace http://tampermonkey.net/
|
||||
// @version 2.1.0
|
||||
|
|
|
|||
Loading…
Reference in New Issue