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==
|
// ==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_xmlhttpRequest */
|
||||||
/* global GM_notification */
|
/* global GM_notification */
|
||||||
(function () {
|
(function () {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,20 @@
|
||||||
// ==COMMAND PARSER START==
|
// ==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 () {
|
(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 {
|
class CommandParser {
|
||||||
static REQUIRED = {
|
static REQUIRED = {
|
||||||
get_file: ['action', 'repo', 'path'],
|
get_file: ['action', 'repo', 'path'],
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
// ==DEBUG PANEL START==
|
// ==DEBUG PANEL START==
|
||||||
|
// Module: debug-panel.js
|
||||||
// Depends on: config.js, logger.js, queue.js, storage.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 */
|
/* global GM_notification */
|
||||||
(function () {
|
(function () {
|
||||||
const cfg = () => window.AI_REPO_CONFIG;
|
const cfg = () => window.AI_REPO_CONFIG;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
// ==DETECTOR START==
|
// ==DETECTOR START==
|
||||||
|
// Module: detector.js
|
||||||
// Depends on: config.js, logger.js, queue.js, command-parser.js, command-executor.js, storage.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 () {
|
(function () {
|
||||||
const cfg = () => window.AI_REPO_CONFIG;
|
const cfg = () => window.AI_REPO_CONFIG;
|
||||||
const log = () => window.AI_REPO_LOGGER;
|
const log = () => window.AI_REPO_LOGGER;
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,72 @@
|
||||||
// ==FINGERPRINT (drop-in utility) ==
|
// ==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(){
|
(function(){
|
||||||
|
/**
|
||||||
|
* CSS selectors that identify assistant messages across supported sites.
|
||||||
|
* These are joined with "," and used for querySelectorAll when scanning neighbors.
|
||||||
|
*/
|
||||||
const MSG_SELECTORS = [
|
const MSG_SELECTORS = [
|
||||||
'[data-message-author-role="assistant"]',
|
'[data-message-author-role="assistant"]',
|
||||||
'.chat-message:not([data-message-author-role="user"])',
|
'.chat-message:not([data-message-author-role="user"])',
|
||||||
'.message-content'
|
'.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(); }
|
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); }
|
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){
|
function commandLikeText(el){
|
||||||
const blocks = el.querySelectorAll('pre code, pre, code');
|
const blocks = el.querySelectorAll('pre code, pre, code');
|
||||||
for (const b of blocks) {
|
for (const b of blocks) {
|
||||||
const t = norm(b.textContent || '');
|
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;
|
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));
|
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) {
|
function prevContextHash(el) {
|
||||||
const list = Array.from(document.querySelectorAll(MSG_SELECTORS.join(',')));
|
const list = Array.from(document.querySelectorAll(MSG_SELECTORS.join(',')));
|
||||||
const idx = list.indexOf(el); if (idx <= 0) return '0';
|
const idx = list.indexOf(el); if (idx <= 0) return '0';
|
||||||
|
|
@ -29,6 +78,12 @@
|
||||||
return hash(buf.slice(-2000));
|
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){
|
function intraPrefixHash(el){
|
||||||
const t = el.textContent || '';
|
const t = el.textContent || '';
|
||||||
const m = t.match(/@bridge@[\s\S]*?@end@/m);
|
const m = t.match(/@bridge@[\s\S]*?@end@/m);
|
||||||
|
|
@ -36,6 +91,12 @@
|
||||||
return hash(norm(t.slice(Math.max(0, endIdx - 2000), endIdx)));
|
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) {
|
function domHint(node) {
|
||||||
if (!node) return '';
|
if (!node) return '';
|
||||||
const id = node.id || '';
|
const id = node.id || '';
|
||||||
|
|
@ -43,6 +104,14 @@
|
||||||
return `${node.tagName || ''}#${id}.${cls}`.slice(0, 40);
|
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) {
|
function ordinalForKey(el, key) {
|
||||||
const list = Array.from(document.querySelectorAll(MSG_SELECTORS.join(',')));
|
const list = Array.from(document.querySelectorAll(MSG_SELECTORS.join(',')));
|
||||||
let n = 0;
|
let n = 0;
|
||||||
|
|
@ -61,6 +130,12 @@
|
||||||
return n;
|
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){
|
function fingerprintElement(el){
|
||||||
const ch = hash(commandLikeText(el).slice(0, 2000));
|
const ch = hash(commandLikeText(el).slice(0, 2000));
|
||||||
const ph = prevContextHash(el);
|
const ph = prevContextHash(el);
|
||||||
|
|
@ -71,6 +146,12 @@
|
||||||
return `${key}|hint:${dh}|n:${n}`;
|
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) {
|
function getStableFingerprint(el) {
|
||||||
if (el?.dataset?.aiRcStableFp) return el.dataset.aiRcStableFp;
|
if (el?.dataset?.aiRcStableFp) return el.dataset.aiRcStableFp;
|
||||||
const fp = fingerprintElement(el);
|
const fp = fingerprintElement(el);
|
||||||
|
|
@ -78,7 +159,7 @@
|
||||||
return fp;
|
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_FINGERPRINT = fingerprintElement;
|
||||||
window.AI_REPO_STABLE_FINGERPRINT = getStableFingerprint;
|
window.AI_REPO_STABLE_FINGERPRINT = getStableFingerprint;
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
// ==MAIN START==
|
// ==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 () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
// ==PASTE SUBMIT START==
|
// ==PASTE SUBMIT START==
|
||||||
|
// Module: paste-submit.js
|
||||||
// Depends on: config.js, logger.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 */
|
/* global GM_setClipboard */
|
||||||
(function () {
|
(function () {
|
||||||
const cfg = () => window.AI_REPO_CONFIG;
|
const cfg = () => window.AI_REPO_CONFIG;
|
||||||
|
|
|
||||||
12
src/queue.js
12
src/queue.js
|
|
@ -1,6 +1,18 @@
|
||||||
// ==QUEUE START==
|
// ==QUEUE START==
|
||||||
|
// Module: queue.js
|
||||||
// Depends on: config.js, logger.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 () {
|
(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 {
|
class ExecutionQueue {
|
||||||
constructor(opts = {}) {
|
constructor(opts = {}) {
|
||||||
const cfg = window.AI_REPO_CONFIG;
|
const cfg = window.AI_REPO_CONFIG;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,18 @@
|
||||||
// ==RESPONSE BUFFER START==
|
// ==RESPONSE BUFFER START==
|
||||||
|
// Module: response-buffer.js
|
||||||
// Depends on: config.js, logger.js, queue.js, paste-submit.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 () {
|
(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) {
|
function chunkByLines(s, limit) {
|
||||||
const out = []; let start = 0;
|
const out = []; let start = 0;
|
||||||
while (start < s.length) {
|
while (start < s.length) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,15 @@
|
||||||
// ==STORAGE START==
|
// ==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 () {
|
(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 {
|
class ConversationHistory {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.conversationId = this._getConversationId();
|
this.conversationId = this._getConversationId();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
|
// Bootstrap loader: this userscript pulls in all modules via @require in dependency order.
|
||||||
// @name AI Repo Commander (Full Features)
|
// @name AI Repo Commander (Full Features)
|
||||||
// @namespace http://tampermonkey.net/
|
// @namespace http://tampermonkey.net/
|
||||||
// @version 2.1.0
|
// @version 2.1.0
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue