66 * move, delete, and undo behaviors.
77 */
88
9+ // Effective resolution logic — loaded from /effective.mjs before app.js
10+ const { EFFECTIVE_RULES , hasEffectiveRule, getAncestorScopes : _getAncestorScopes ,
11+ computeEffectiveSets : _computeEffectiveSets , getEffectiveItems } = window . Effective ;
12+
13+ // Helper: get effectiveRule for a category (returns string or undefined)
14+ function getEffectiveRule ( category ) { return EFFECTIVE_RULES [ category ] || null ; }
15+
916let data = null ;
1017let activeFilters = new Set ( ) ;
1118let selectedItem = null ;
@@ -37,28 +44,17 @@ const uiState = {
3744const CATEGORY_ORDER = [ "skill" , "memory" , "mcp" , "command" , "agent" , "plan" , "rule" , "config" , "hook" , "plugin" , "session" ] ;
3845
3946const CATEGORIES = {
40- memory : { icon : "🧠" , label : "Memories" , filterLabel : "Memories" , group : "memory" ,
41- effectiveRule : "Global memories are available in all projects; project memories are specific to this project" } ,
42- skill : { icon : "⚡" , label : "Skills" , filterLabel : "Skills" , group : "skill" ,
43- effectiveRule : "Available from Personal (~/.claude/skills), Project (.claude/skills), and installed Plugins" } ,
44- session :{ icon : "💬" , label : "Sessions" , filterLabel : "Sessions" , group : null ,
45- effectiveRule : null } , // project-only, no inheritance
46- mcp : { icon : "🔌" , label : "MCP Servers" , filterLabel : "MCP" , group : "mcp" ,
47- effectiveRule : "Resolved by local > project > user — same-name servers use the narrower scope" } ,
48- command :{ icon : "▶️" , label : "Commands" , filterLabel : "Commands" , group : "command" ,
49- effectiveRule : "Available from User and Project — same-name conflicts are not supported" } ,
50- agent : { icon : "🤖" , label : "Agents" , filterLabel : "Agents" , group : "agent" ,
51- effectiveRule : "Project-level agents override same-name User agents" } ,
52- plan : { icon : "📐" , label : "Plans" , filterLabel : "Plans" , group : "plan" ,
53- effectiveRule : null } , // no clear official scope rule
54- rule : { icon : "📏" , label : "Rules" , filterLabel : "Rules" , group : null ,
55- effectiveRule : null } , // no clear official scope rule
56- config : { icon : "⚙️" , label : "Config" , filterLabel : "Config" , group : null ,
57- effectiveRule : "Resolved by precedence: managed > CLI > project local > project shared > user" } ,
58- hook : { icon : "🪝" , label : "Hooks" , filterLabel : "Hooks" , group : null ,
59- effectiveRule : "Configured in settings files — resolved by settings precedence" } ,
60- plugin : { icon : "🧩" , label : "Plugins" , filterLabel : "Plugins" , group : null ,
61- effectiveRule : null } , // global-only, no inheritance rule
47+ memory : { icon : "🧠" , label : "Memories" , filterLabel : "Memories" , group : "memory" } ,
48+ skill : { icon : "⚡" , label : "Skills" , filterLabel : "Skills" , group : "skill" } ,
49+ session :{ icon : "💬" , label : "Sessions" , filterLabel : "Sessions" , group : null } ,
50+ mcp : { icon : "🔌" , label : "MCP Servers" , filterLabel : "MCP" , group : "mcp" } ,
51+ command :{ icon : "▶️" , label : "Commands" , filterLabel : "Commands" , group : "command" } ,
52+ agent : { icon : "🤖" , label : "Agents" , filterLabel : "Agents" , group : "agent" } ,
53+ plan : { icon : "📐" , label : "Plans" , filterLabel : "Plans" , group : "plan" } ,
54+ rule : { icon : "📏" , label : "Rules" , filterLabel : "Rules" , group : null } ,
55+ config : { icon : "⚙️" , label : "Config" , filterLabel : "Config" , group : null } ,
56+ hook : { icon : "🪝" , label : "Hooks" , filterLabel : "Hooks" , group : null } ,
57+ plugin : { icon : "🧩" , label : "Plugins" , filterLabel : "Plugins" , group : null } ,
6258} ;
6359
6460const ITEM_ICONS = {
@@ -696,7 +692,7 @@ function renderPills() {
696692 : data ?. items || [ ] ;
697693 if ( showEffective && selectedScopeId && selectedScopeId !== "global" ) {
698694 const globalItems = ( data ?. items || [ ] ) . filter (
699- ( i ) => i . scopeId === "global" && Boolean ( CATEGORIES [ i . category ] ?. effectiveRule )
695+ ( i ) => i . scopeId === "global" && Boolean ( getEffectiveRule ( i . category ) )
700696 ) ;
701697 scopeItems = [ ...scopeItems , ...globalItems ] ;
702698 }
@@ -712,13 +708,13 @@ function renderPills() {
712708 { key : "all" , label : "All" , icon : "◌" , count : scopeTotal , tip : null } ,
713709 ...CATEGORY_ORDER . map ( ( category ) => {
714710 const config = CATEGORIES [ category ] || { icon : "📄" , filterLabel : capitalize ( category ) } ;
715- const hasRule = Boolean ( config . effectiveRule ) ;
711+ const hasRule = Boolean ( getEffectiveRule ( category ) ) ;
716712 return {
717713 key : category ,
718714 label : config . filterLabel ,
719715 icon : config . icon ,
720716 count : scopeCounts [ category ] || 0 ,
721- tip : config . effectiveRule || ( showEffective ? NO_RULE_TIP : null ) ,
717+ tip : getEffectiveRule ( category ) || ( showEffective ? NO_RULE_TIP : null ) ,
722718 noRule : showEffective && ! hasRule ,
723719 } ;
724720 } ) ,
@@ -777,7 +773,7 @@ function renderRuleBar() {
777773 const rows = CATEGORY_ORDER . map ( cat => {
778774 const config = CATEGORIES [ cat ] ;
779775 if ( ! config ) return "" ;
780- const rule = config . effectiveRule ;
776+ const rule = getEffectiveRule ( cat ) ;
781777 const noRule = ! rule ;
782778 return `<div class="rule-row${ noRule ? " rule-none" : "" } ">
783779 <span class="rule-cat">${ config . icon } ${ esc ( config . filterLabel ) } </span>
@@ -1096,7 +1092,7 @@ function renderEffectiveBehavior(item) {
10961092 : "This memory is stored in this project's memory directory." ;
10971093 break ;
10981094 default :
1099- why = CATEGORIES [ item . category ] ?. effectiveRule || "" ;
1095+ why = getEffectiveRule ( item . category ) || "" ;
11001096 }
11011097
11021098 if ( ! why ) { wrap . classList . add ( "hidden" ) ; return ; }
@@ -2510,82 +2506,37 @@ function findVisibleScopeInTree(scope) {
25102506 * and which items have unresolvable name conflicts (commands).
25112507 * Called whenever showEffective toggles or scope changes.
25122508 */
2509+ // getAncestorScopes — delegated to shared effective.mjs (_getAncestorScopes)
2510+
25132511/**
2514- * Returns scopes whose repoDir is a path ancestor of the given scope's repoDir.
2515- * e.g. if scopeId is /home/user/company/repo, returns the scope for /home/user/company if it exists .
2512+ * Wrapper: delegates to shared effective.mjs module for computation,
2513+ * then stores results in app-level state (effectiveShadowedKeys etc) .
25162514 */
2517- function getAncestorScopes ( scopeId ) {
2518- const scope = getScopeById ( scopeId ) ;
2519- if ( ! scope ?. repoDir ) return [ ] ;
2520- return ( data ?. scopes || [ ] ) . filter ( s =>
2521- s . repoDir &&
2522- s . id !== scopeId &&
2523- s . id !== "global" &&
2524- scope . repoDir . startsWith ( s . repoDir + "/" )
2525- ) ;
2526- }
2527-
25282515function computeEffectiveSets ( scopeId ) {
2529- effectiveShadowedKeys = new Set ( ) ;
2530- effectiveConflictKeys = new Set ( ) ;
2531- effectiveAncestorKeys = new Set ( ) ;
2532- if ( ! showEffective || ! scopeId || scopeId === "global" ) return ;
2533-
2534- const projectItems = getItemsForScope ( scopeId ) ;
2535- const globalItems = getItemsForScope ( "global" ) ;
2536-
2537- // MCP & Agents: narrower scope (project) wins same-name
2538- for ( const cat of [ "mcp" , "agent" ] ) {
2539- const projectNames = new Set (
2540- projectItems . filter ( i => i . category === cat ) . map ( i => i . name )
2541- ) ;
2542- for ( const gi of globalItems . filter ( i => i . category === cat ) ) {
2543- if ( projectNames . has ( gi . name ) ) effectiveShadowedKeys . add ( itemKey ( gi ) ) ;
2544- }
2545- }
2546-
2547- // Commands: both levels available but same-name conflicts are officially unsupported
2548- const projCmdNames = new Set ( projectItems . filter ( i => i . category === "command" ) . map ( i => i . name ) ) ;
2549- const globalCmdNames = new Set ( globalItems . filter ( i => i . category === "command" ) . map ( i => i . name ) ) ;
2550- for ( const name of projCmdNames ) {
2551- if ( ! globalCmdNames . has ( name ) ) continue ;
2552- for ( const i of [ ...projectItems , ...globalItems ] . filter ( i => i . category === "command" && i . name === name ) ) {
2553- effectiveConflictKeys . add ( itemKey ( i ) ) ;
2554- }
2555- }
2556-
2557- // Ancestor scopes: scopes whose repoDir is a path parent of this project.
2558- // Their config items (especially CLAUDE.md) are ancestor-loaded by Claude Code.
2559- const ancestorScopes = getAncestorScopes ( scopeId ) ;
2560- for ( const as of ancestorScopes ) {
2561- for ( const i of getItemsForScope ( as . id ) . filter ( i => i . category === "config" || i . category === "memory" ) ) {
2562- effectiveAncestorKeys . add ( itemKey ( i ) ) ;
2563- }
2516+ if ( ! showEffective || ! scopeId || scopeId === "global" ) {
2517+ effectiveShadowedKeys = new Set ( ) ;
2518+ effectiveConflictKeys = new Set ( ) ;
2519+ effectiveAncestorKeys = new Set ( ) ;
2520+ return ;
25642521 }
2522+ const result = _computeEffectiveSets ( scopeId , data ?. items || [ ] , data ?. scopes || [ ] , itemKey ) ;
2523+ effectiveShadowedKeys = result . shadowedKeys ;
2524+ effectiveConflictKeys = result . conflictKeys ;
2525+ effectiveAncestorKeys = result . ancestorKeys ;
25652526}
25662527
25672528function getVisibleItemsForScope ( scopeId ) {
25682529 const ownItems = getItemsForScope ( scopeId ) . filter ( ( item ) => itemVisibleInMain ( item ) ) ;
25692530 if ( ! showEffective || scopeId === "global" ) return ownItems ;
25702531
2571- // Only add global items for categories that have an official effective rule
2572- const globalItems = getItemsForScope ( "global" ) . filter ( ( item ) => {
2573- if ( ! itemMatchesFilters ( item ) || ! itemMatchesSearch ( item ) ) return false ;
2574- return Boolean ( CATEGORIES [ item . category ] ?. effectiveRule ) ;
2575- } ) ;
2576-
2577- // Add ancestor scope items (CLAUDE.md + memories from path-parent projects)
2578- const ancestorItems = [ ] ;
2579- for ( const as of getAncestorScopes ( scopeId ) ) {
2580- for ( const item of getItemsForScope ( as . id ) ) {
2581- if ( ( item . category === "config" || item . category === "memory" ) &&
2582- itemMatchesFilters ( item ) && itemMatchesSearch ( item ) ) {
2583- ancestorItems . push ( item ) ;
2584- }
2585- }
2586- }
2587-
2588- return [ ...ownItems , ...globalItems , ...ancestorItems ] ;
2532+ // Use shared module for effective resolution, then apply UI filters
2533+ const allEffective = getEffectiveItems ( scopeId , data ?. items || [ ] , data ?. scopes || [ ] ) ;
2534+ // Filter to non-own items that pass current UI filters
2535+ const ownKeys = new Set ( ownItems . map ( i => itemKey ( i ) ) ) ;
2536+ const extra = allEffective . filter ( i =>
2537+ ! ownKeys . has ( itemKey ( i ) ) && itemMatchesFilters ( i ) && itemMatchesSearch ( i )
2538+ ) ;
2539+ return [ ...ownItems , ...extra ] ;
25892540}
25902541
25912542function itemVisibleInMain ( item ) {
0 commit comments