Compare commits
3 Commits
f49d61f14d
...
c408289e0f
| Author | SHA1 | Date |
|---|---|---|
|
|
c408289e0f | |
|
|
b3a6986dc5 | |
|
|
0ac583fec4 |
|
|
@ -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'],
|
||||||
|
|
|
||||||
110
src/config.js
110
src/config.js
|
|
@ -1,77 +1,105 @@
|
||||||
// ==CONFIG START==
|
// ==CONFIG START==
|
||||||
(function () {
|
(function () {
|
||||||
|
// LocalStorage keys used by this userscript. These names are stable across versions.
|
||||||
|
// - history: per-conversation dedupe records (fingerprints of executed commands)
|
||||||
|
// - cfg: persisted user configuration (excluding transient runtime flags)
|
||||||
|
// - panel: debug panel UI state (position, collapsed state, selected tab)
|
||||||
const STORAGE_KEYS = {
|
const STORAGE_KEYS = {
|
||||||
history: 'ai_repo_commander_executed',
|
history: 'ai_repo_commander_executed',
|
||||||
cfg: 'ai_repo_commander_cfg',
|
cfg: 'ai_repo_commander_cfg',
|
||||||
panel: 'ai_repo_commander_panel_state'
|
panel: 'ai_repo_commander_panel_state'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DEFAULT_CONFIG holds all tunable settings for AI Repo Commander.
|
||||||
|
* Access values via: window.AI_REPO_CONFIG.get('path.to.key')
|
||||||
|
* Update at runtime via: window.AI_REPO_CONFIG.set('path.to.key', value)
|
||||||
|
*
|
||||||
|
* Sections:
|
||||||
|
* - meta: Script metadata (version only)
|
||||||
|
* - api: Bridge/API behavior (timeouts, retries, key)
|
||||||
|
* - debug: Logging and debug panel settings
|
||||||
|
* - execution: Detector and processing timings / hardening
|
||||||
|
* - queue: Rate limiting for executing commands
|
||||||
|
* - ui: Paste/submit behavior and UX toggles
|
||||||
|
* - storage: Dedupe/cleanup persistence settings
|
||||||
|
* - response: Paste buffer behavior for long responses
|
||||||
|
* - runtime: Transient flags (not persisted)
|
||||||
|
*/
|
||||||
const DEFAULT_CONFIG = {
|
const DEFAULT_CONFIG = {
|
||||||
|
// Script/version metadata. Not used for logic; useful for UI and logs.
|
||||||
meta: { version: '1.6.2' },
|
meta: { version: '1.6.2' },
|
||||||
|
|
||||||
|
// Bridge/API call settings used by command-executor.js
|
||||||
api: {
|
api: {
|
||||||
enabled: true,
|
enabled: true, // Master switch: if false, actions are mocked locally (no network)
|
||||||
timeout: 60000,
|
timeout: 60000, // Request timeout for GM_xmlhttpRequest, in milliseconds
|
||||||
maxRetries: 2,
|
maxRetries: 2, // Number of retries after the initial attempt (total attempts = 1 + maxRetries)
|
||||||
bridgeKey: ''
|
bridgeKey: '' // Secret key sent as X-Bridge-Key header. Prompted if empty when enabled
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Debug logging configuration and debug panel behavior
|
||||||
debug: {
|
debug: {
|
||||||
enabled: true,
|
enabled: true, // Toggle logging/panel features globally
|
||||||
level: 3, // 0=off, 1=errors, 2=warn, 3=info, 4=verbose, 5=trace
|
level: 3, // 0=off, 1=error, 2=warn, 3=info, 4=verbose, 5=trace (see logger.js)
|
||||||
watchMs: 120000,
|
watchMs: 120000, // Time window used by Logger.logLoop to limit repeated logs
|
||||||
maxLines: 400,
|
maxLines: 400, // Max log entries kept in memory (oldest are dropped)
|
||||||
showPanel: true
|
showPanel: true // Show the draggable Logs/Tools panel when true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Execution hardening and detector timing options (see detector.js)
|
||||||
execution: {
|
execution: {
|
||||||
debounceDelay: 6500,
|
debounceDelay: 6500, // Wait after a new assistant message to allow streaming to finish
|
||||||
settleCheckMs: 1300,
|
settleCheckMs: 1300, // Stable-window length to consider text "settled" after last change
|
||||||
settlePollMs: 250,
|
settlePollMs: 250, // How often to poll the DOM during the settle window
|
||||||
requireTerminator: true,
|
requireTerminator: true, // Require @end@ terminator inside blocks before attempting to execute
|
||||||
coldStartMs: 2000,
|
coldStartMs: 2000, // Initial delay after page load to avoid immediate re-execution
|
||||||
stuckAfterMs: 10 * 60 * 1000,
|
stuckAfterMs: 10 * 60 * 1000, // Consider a long-running flow "stuck" after this time (for warnings)
|
||||||
scanDebounceMs: 400,
|
scanDebounceMs: 400, // Debounce for scanning existing content or rapid changes
|
||||||
fastWarnMs: 50,
|
fastWarnMs: 50, // Threshold for logging fast operations as timing markers
|
||||||
slowWarnMs: 60000,
|
slowWarnMs: 60000, // Threshold for warning on slow operations
|
||||||
clusterRescanMs: 1000,
|
clusterRescanMs: 1000, // Interval to rescan neighboring assistant messages for chained blocks
|
||||||
clusterMaxLookahead: 3
|
clusterMaxLookahead: 3 // How many subsequent assistant messages to peek when clustering
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Queue/rate-limiting settings (see queue.js)
|
||||||
queue: {
|
queue: {
|
||||||
minDelayMs: 1500,
|
minDelayMs: 1500, // Minimum delay between two executed commands
|
||||||
maxPerMinute: 15,
|
maxPerMinute: 15, // Rate cap: maximum commands started per rolling minute
|
||||||
maxPerMessage: 5,
|
maxPerMessage: 5, // Safety limit: maximum commands taken from a single assistant message
|
||||||
waitForComposerMs: 12000
|
waitForComposerMs: 12000 // How long to wait for the chat composer to be ready before giving up
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// UI behavior around pasting into the composer and submitting
|
||||||
ui: {
|
ui: {
|
||||||
autoSubmit: true,
|
autoSubmit: true, // If true, attempt to submit after pasting (button click or Enter key)
|
||||||
appendTrailingNewline: true,
|
appendTrailingNewline: true, // Append a newline to pasted content to preserve code fences in some editors
|
||||||
postPasteDelayMs: 600,
|
postPasteDelayMs: 600, // Small delay after paste before trying to click Send/press Enter
|
||||||
showExecutedMarker: true,
|
showExecutedMarker: true, // Visually mark messages that had commands executed (left border/title)
|
||||||
processExisting: false,
|
processExisting: false, // On init, optionally scan and process messages already on the page
|
||||||
submitMode: 'button_first',
|
submitMode: 'button_first', // Submit strategy: 'button_first' tries button, then falls back to Enter
|
||||||
maxComposerWaitMs: 15 * 60 * 1000,
|
maxComposerWaitMs: 15 * 60 * 1000, // Global max wait for composer availability in edge cases
|
||||||
submitMaxRetries: 12
|
submitMaxRetries: 12 // How many times to retry submit attempts (e.g., flaky Send button)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Persistence and housekeeping settings for localStorage
|
||||||
storage: {
|
storage: {
|
||||||
dedupeTtlMs: 30 * 24 * 60 * 60 * 1000, // 30 days
|
dedupeTtlMs: 30 * 24 * 60 * 60 * 1000, // 30 days; prevent re-execution in the same conversation within TTL
|
||||||
cleanupAfterMs: 30000,
|
cleanupAfterMs: 30000, // Delay before running a cleanup pass after startup
|
||||||
cleanupIntervalMs: 60000
|
cleanupIntervalMs: 60000 // How frequently to run periodic cleanup of stale records
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Response paste buffer settings (see response-buffer.js)
|
||||||
response: {
|
response: {
|
||||||
bufferFlushDelayMs: 500,
|
bufferFlushDelayMs: 500, // Delay before flushing buffered chunks to paste (to batch sibling results)
|
||||||
sectionHeadings: true,
|
sectionHeadings: true, // When true, prepend small headings when pasting multiple sections
|
||||||
maxPasteChars: 250000,
|
maxPasteChars: 250000, // Maximum characters to paste at once; larger results are split
|
||||||
splitLongResponses: true
|
splitLongResponses: true // Split long responses into multiple pastes when exceeding maxPasteChars
|
||||||
},
|
},
|
||||||
|
|
||||||
// Runtime state (not persisted)
|
// Runtime state (not persisted) — toggled by UI/console helpers
|
||||||
runtime: {
|
runtime: {
|
||||||
paused: false
|
paused: false // When true, detectors ignore new mutations; queue continues unless stopped
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,6 +1,52 @@
|
||||||
// ==LOGGER START==
|
// ==LOGGER START==
|
||||||
(function () {
|
(function () {
|
||||||
|
/**
|
||||||
|
* AI Repo Commander — Logger module
|
||||||
|
*
|
||||||
|
* Purpose
|
||||||
|
* - Provide structured, level-gated logging for all modules.
|
||||||
|
* - Buffer recent log entries for the in-page debug panel and copy/export.
|
||||||
|
* - Prevent log spam from hot paths via logLoop.
|
||||||
|
*
|
||||||
|
* Integration
|
||||||
|
* - Exposed as window.AI_REPO_LOGGER for use across modules.
|
||||||
|
* - Uses window.AI_REPO_CONFIG for runtime toggles and limits:
|
||||||
|
* - debug.enabled: boolean gate for all logging
|
||||||
|
* - debug.level: 0–5 (0=off, 1=error, 2=warn, 3=info, 4=verbose, 5=trace)
|
||||||
|
* - debug.watchMs: time window used by logLoop for anti-spam
|
||||||
|
* - debug.maxLines: max entries retained in memory buffer
|
||||||
|
*
|
||||||
|
* Console format
|
||||||
|
* - Each entry prints to the browser console as: [AI RC <LEVEL>] <message> <data?>
|
||||||
|
* - Data is sanitized to avoid dumping large strings or DOM nodes.
|
||||||
|
*
|
||||||
|
* Notes
|
||||||
|
* - No external dependencies, works in plain browser context.
|
||||||
|
* - This module does not persist logs; it keeps an in-memory ring buffer only.
|
||||||
|
*
|
||||||
|
* Example
|
||||||
|
* const log = window.AI_REPO_LOGGER;
|
||||||
|
* log.info('Starting');
|
||||||
|
* log.warn('Slow operation', { ms: 1234 });
|
||||||
|
* log.error('Failed', { error: e.message });
|
||||||
|
* console.log(log.getRecentLogs(100));
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structured logger with in-memory buffer and anti-spam utilities.
|
||||||
|
* Fields:
|
||||||
|
* - config: ConfigManager; access to debug.* keys
|
||||||
|
* - buffer: Array<{timestamp, level, message, data}> recent log entries
|
||||||
|
* - loopCounts: Map used by logLoop to cap repeated messages
|
||||||
|
* - startedAt: number (ms) reference time for logLoop watch window
|
||||||
|
*/
|
||||||
class Logger {
|
class Logger {
|
||||||
|
/**
|
||||||
|
* Initializes the logger.
|
||||||
|
* - Grabs the global config instance.
|
||||||
|
* - Prepares the in-memory buffer and loop counters.
|
||||||
|
* - Starts a periodic cleanup that resets logLoop counters after ~2× watch window.
|
||||||
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
this.config = window.AI_REPO_CONFIG;
|
this.config = window.AI_REPO_CONFIG;
|
||||||
this.buffer = [];
|
this.buffer = [];
|
||||||
|
|
@ -17,6 +63,8 @@
|
||||||
}, this.config.get('debug.watchMs') || 120000);
|
}, this.config.get('debug.watchMs') || 120000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convenience level helpers. Gated by debug.enabled and debug.level (see _log).
|
||||||
|
// Levels: 1=ERROR, 2=WARN, 3=INFO, 4=VERBOSE, 5=TRACE
|
||||||
error(msg, data) { this._log(1, 'ERROR', msg, data); }
|
error(msg, data) { this._log(1, 'ERROR', msg, data); }
|
||||||
warn(msg, data) { this._log(2, 'WARN', msg, data); }
|
warn(msg, data) { this._log(2, 'WARN', msg, data); }
|
||||||
info(msg, data) { this._log(3, 'INFO', msg, data); }
|
info(msg, data) { this._log(3, 'INFO', msg, data); }
|
||||||
|
|
@ -80,6 +128,24 @@
|
||||||
else this.info(`${msg}${suffix}`);
|
else this.info(`${msg}${suffix}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core logging sink. Applies gating, buffers the entry, and prints to console.
|
||||||
|
*
|
||||||
|
* Gating
|
||||||
|
* - If debug.enabled is false, nothing is logged.
|
||||||
|
* - If levelNum > debug.level, the entry is ignored.
|
||||||
|
*
|
||||||
|
* Buffering
|
||||||
|
* - Recent entries kept in memory (debug.maxLines ring buffer) for the debug panel and copy/export.
|
||||||
|
*
|
||||||
|
* Console output
|
||||||
|
* - Printed as: [AI RC <LEVEL>] message [sanitized data]
|
||||||
|
*
|
||||||
|
* @param {number} levelNum - Numeric level (1..5)
|
||||||
|
* @param {string} levelName - Label shown in output (ERROR/WARN/INFO/VERBOSE/TRACE)
|
||||||
|
* @param {any} msg - Primary message; coerced to string
|
||||||
|
* @param {any} [data] - Optional context data; sanitized to avoid huge strings/DOM elements
|
||||||
|
*/
|
||||||
_log(levelNum, levelName, msg, data) {
|
_log(levelNum, levelName, msg, data) {
|
||||||
const enabled = !!this.config.get('debug.enabled');
|
const enabled = !!this.config.get('debug.enabled');
|
||||||
const level = this.config.get('debug.level') ?? 0;
|
const level = this.config.get('debug.level') ?? 0;
|
||||||
|
|
@ -99,6 +165,19 @@
|
||||||
entry.data ? console.log(prefix, msg, entry.data) : console.log(prefix, msg);
|
entry.data ? console.log(prefix, msg, entry.data) : console.log(prefix, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Best-effort redaction/sanitization of context data before logging.
|
||||||
|
* - HTMLElement → "HTMLElement<TAG>" (prevents dumping live DOM trees)
|
||||||
|
* - Long strings (>200 chars) → truncated with ellipsis to keep logs concise
|
||||||
|
* - Plain objects → shallowly sanitize each value using the same rules
|
||||||
|
* - Other primitives are returned as-is
|
||||||
|
*
|
||||||
|
* This keeps console output readable and reduces accidental leakage of
|
||||||
|
* large payloads while still conveying useful context.
|
||||||
|
*
|
||||||
|
* @param {any} data
|
||||||
|
* @returns {any} sanitized data suitable for console/JSON
|
||||||
|
*/
|
||||||
_sanitize(data) {
|
_sanitize(data) {
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
if (data instanceof HTMLElement) return `HTMLElement<${data.tagName}>`;
|
if (data instanceof HTMLElement) return `HTMLElement<${data.tagName}>`;
|
||||||
|
|
@ -114,12 +193,29 @@
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the most recent N logs as plain text, one entry per line.
|
||||||
|
* Each line format: ISO_TIMESTAMP LEVEL message {jsonData?}
|
||||||
|
*
|
||||||
|
* @param {number} [n=50] - Number of lines to include from the tail of the buffer
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
getRecentLogs(n = 50) {
|
getRecentLogs(n = 50) {
|
||||||
return this.buffer.slice(-n).map(e =>
|
return this.buffer.slice(-n).map(e =>
|
||||||
`${e.timestamp} ${e.level.padEnd(7)} ${e.message}${e.data ? ' ' + JSON.stringify(e.data) : ''}`
|
`${e.timestamp} ${e.level.padEnd(7)} ${e.message}${e.data ? ' ' + JSON.stringify(e.data) : ''}`
|
||||||
).join('\n');
|
).join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the runtime log level in config (0–5) and logs the change.
|
||||||
|
* Levels:
|
||||||
|
* 0=off, 1=error, 2=warn, 3=info, 4=verbose, 5=trace
|
||||||
|
*
|
||||||
|
* Note: The debug panel level selector typically writes to the same key.
|
||||||
|
* This method is provided for console/automation convenience.
|
||||||
|
*
|
||||||
|
* @param {number} n - Desired level; values are clamped to [0,5]
|
||||||
|
*/
|
||||||
setLevel(n) {
|
setLevel(n) {
|
||||||
const lv = Math.max(0, Math.min(5, n));
|
const lv = Math.max(0, Math.min(5, n));
|
||||||
this.config.set('debug.level', lv);
|
this.config.set('debug.level', lv);
|
||||||
|
|
|
||||||
|
|
@ -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