@@ -10,7 +10,11 @@ let data = null;
1010let activeFilters = new Set ( ) ;
1111let selectedItem = null ;
1212let selectedScopeId = null ;
13- let showInherited = false ;
13+ let showEffective = false ;
14+ // Keys of global items that are shadowed by a project item with the same name
15+ let effectiveShadowedKeys = new Set ( ) ;
16+ // Keys of items that have a same-name conflict (commands: not reliably overridable)
17+ let effectiveConflictKeys = new Set ( ) ;
1418let pendingDrag = null ;
1519let pendingDelete = null ;
1620let draggingItem = null ;
@@ -30,17 +34,28 @@ const uiState = {
3034const CATEGORY_ORDER = [ "skill" , "memory" , "mcp" , "command" , "agent" , "plan" , "rule" , "config" , "hook" , "plugin" , "session" ] ;
3135
3236const CATEGORIES = {
33- memory : { icon : "🧠" , label : "Memories" , filterLabel : "Memories" , group : "memory" } ,
34- skill : { icon : "⚡" , label : "Skills" , filterLabel : "Skills" , group : "skill" } ,
35- session : { icon : "💬" , label : "Sessions" , filterLabel : "Sessions" , group : null } ,
36- mcp : { icon : "🔌" , label : "MCP Servers" , filterLabel : "MCP" , group : "mcp" } ,
37- command : { icon : "▶️" , label : "Commands" , filterLabel : "Commands" , group : "command" } ,
38- agent : { icon : "🤖" , label : "Agents" , filterLabel : "Agents" , group : "agent" } ,
39- plan : { icon : "📐" , label : "Plans" , filterLabel : "Plans" , group : "plan" } ,
40- rule : { icon : "📏" , label : "Rules" , filterLabel : "Rules" , group : null } ,
41- config : { icon : "⚙️" , label : "Config" , filterLabel : "Config" , group : null } ,
42- hook : { icon : "🪝" , label : "Hooks" , filterLabel : "Hooks" , group : null } ,
43- plugin : { icon : "🧩" , label : "Plugins" , filterLabel : "Plugins" , group : null } ,
37+ memory : { icon : "🧠" , label : "Memories" , filterLabel : "Memories" , group : "memory" ,
38+ effectiveRule : null } , // ancestry requires runtime cwd — not computed
39+ skill : { icon : "⚡" , label : "Skills" , filterLabel : "Skills" , group : "skill" ,
40+ effectiveRule : "Available from Personal (~/.claude/skills), Project (.claude/skills), and installed Plugins" } ,
41+ session :{ icon : "💬" , label : "Sessions" , filterLabel : "Sessions" , group : null ,
42+ effectiveRule : null } , // project-only, no inheritance
43+ mcp : { icon : "🔌" , label : "MCP Servers" , filterLabel : "MCP" , group : "mcp" ,
44+ effectiveRule : "Resolved by local > project > user — same-name servers use the narrower scope" } ,
45+ command :{ icon : "▶️" , label : "Commands" , filterLabel : "Commands" , group : "command" ,
46+ effectiveRule : "Available from User and Project — same-name conflicts are not supported" } ,
47+ agent : { icon : "🤖" , label : "Agents" , filterLabel : "Agents" , group : "agent" ,
48+ effectiveRule : "Project-level agents override same-name User agents" } ,
49+ plan : { icon : "📐" , label : "Plans" , filterLabel : "Plans" , group : "plan" ,
50+ effectiveRule : null } , // no clear official scope rule
51+ rule : { icon : "📏" , label : "Rules" , filterLabel : "Rules" , group : null ,
52+ effectiveRule : null } , // no clear official scope rule
53+ config : { icon : "⚙️" , label : "Config" , filterLabel : "Config" , group : null ,
54+ effectiveRule : "Resolved by precedence: managed > CLI > project local > project shared > user" } ,
55+ hook : { icon : "🪝" , label : "Hooks" , filterLabel : "Hooks" , group : null ,
56+ effectiveRule : "Configured in settings files — resolved by settings precedence" } ,
57+ plugin : { icon : "🧩" , label : "Plugins" , filterLabel : "Plugins" , group : null ,
58+ effectiveRule : null } , // global-only, no inheritance rule
4459} ;
4560
4661const ITEM_ICONS = {
@@ -221,7 +236,7 @@ function setupSidebarTree() {
221236 const hdr = event . target . closest ( ".s-scope-hdr" ) ;
222237 if ( ! hdr ) return ;
223238 selectedScopeId = hdr . dataset . scopeId ;
224- showInherited = false ;
239+ showEffective = false ; effectiveShadowedKeys = new Set ( ) ; effectiveConflictKeys = new Set ( ) ;
225240 expandScopePath ( selectedScopeId ) ;
226241 if ( isContextBudgetOpen ( ) ) {
227242 openContextBudget ( selectedScopeId ) ;
@@ -622,12 +637,14 @@ function renderContentHeader() {
622637
623638function renderPills ( ) {
624639 const container = document . getElementById ( "pills" ) ;
625- // Count items for the currently selected scope, plus Global if showInherited
640+ // Count items for the currently selected scope, plus Global effective items if showEffective
626641 let scopeItems = selectedScopeId
627642 ? ( data ?. items || [ ] ) . filter ( ( i ) => i . scopeId === selectedScopeId )
628643 : data ?. items || [ ] ;
629- if ( showInherited && selectedScopeId && selectedScopeId !== "global" ) {
630- const globalItems = ( data ?. items || [ ] ) . filter ( ( i ) => i . scopeId === "global" ) ;
644+ if ( showEffective && selectedScopeId && selectedScopeId !== "global" ) {
645+ const globalItems = ( data ?. items || [ ] ) . filter (
646+ ( i ) => i . scopeId === "global" && Boolean ( CATEGORIES [ i . category ] ?. effectiveRule )
647+ ) ;
631648 scopeItems = [ ...scopeItems , ...globalItems ] ;
632649 }
633650 const scopeCounts = { } ;
@@ -636,15 +653,20 @@ function renderPills() {
636653 scopeCounts [ item . category ] = ( scopeCounts [ item . category ] || 0 ) + 1 ;
637654 scopeTotal ++ ;
638655 }
656+
657+ const NO_RULE_TIP = "No official scope rule — shown as inventory only" ;
639658 const pills = [
640- { key : "all" , label : "All" , icon : "◌" , count : scopeTotal } ,
659+ { key : "all" , label : "All" , icon : "◌" , count : scopeTotal , tip : null } ,
641660 ...CATEGORY_ORDER . map ( ( category ) => {
642661 const config = CATEGORIES [ category ] || { icon : "📄" , filterLabel : capitalize ( category ) } ;
662+ const hasRule = Boolean ( config . effectiveRule ) ;
643663 return {
644664 key : category ,
645665 label : config . filterLabel ,
646666 icon : config . icon ,
647667 count : scopeCounts [ category ] || 0 ,
668+ tip : config . effectiveRule || ( showEffective ? NO_RULE_TIP : null ) ,
669+ noRule : showEffective && ! hasRule ,
648670 } ;
649671 } ) ,
650672 ] ;
@@ -657,8 +679,10 @@ function renderPills() {
657679 container . innerHTML = `
658680 ${ visiblePills . map ( ( pill ) => {
659681 const isActive = pill . key === "all" ? allActive : activeFilters . has ( pill . key ) ;
682+ const dimmed = pill . noRule ? " f-pill-dim" : "" ;
683+ const titleAttr = pill . tip ? ` title="${ esc ( pill . tip ) } "` : "" ;
660684 return `
661- <button type="button" class="f-pill${ isActive ? " active" : "" } " data-filter="${ pill . key } ">
685+ <button type="button" class="f-pill${ isActive ? " active" : "" } ${ dimmed } " data-filter="${ pill . key } "${ titleAttr } >
662686 <span class="f-pill-ico">${ pill . icon } </span>
663687 ${ esc ( pill . label ) }
664688 <b>${ pill . count } </b>
@@ -797,9 +821,15 @@ function renderItem(item) {
797821 const sizeLabel = item . size || "—" ;
798822 const desc = item . description || item . fileName || item . path || "No description" ;
799823
800- const isInherited = showInherited && item . scopeId === "global" && selectedScopeId !== "global" ;
801- const inheritedBadge = isInherited ? `<span class="item-badge ib-global">Global</span>` : "" ;
802- const actions = ( item . locked || isInherited ) ? "" : `
824+ // Effective-mode status badges
825+ const isFromGlobal = showEffective && item . scopeId === "global" && selectedScopeId !== "global" ;
826+ const isShadowed = isFromGlobal && effectiveShadowedKeys . has ( key ) ;
827+ const isConflict = showEffective && effectiveConflictKeys . has ( key ) ;
828+ const effectiveBadge = isShadowed ? `<span class="item-badge ib-shadowed">Shadowed</span>`
829+ : isConflict ? `<span class="item-badge ib-conflict">Conflict</span>`
830+ : isFromGlobal ? `<span class="item-badge ib-global">Global</span>`
831+ : "" ;
832+ const actions = ( item . locked || isFromGlobal ) ? "" : `
803833 <span class="item-actions">
804834 ${ ( canMoveItem ( item ) || item . locked ) ? `<button type="button" class="act-btn act-move" data-action="move">Move</button>` : "" }
805835 <button type="button" class="act-btn act-open" data-action="open">Open</button>
@@ -829,7 +859,7 @@ function renderItem(item) {
829859 <span class="item-ico">${ icon } </span>
830860 <span class="item-name">${ esc ( item . name ) } </span>
831861 ${ secBadgeHtml } ${ blFlagHtml }
832- ${ inheritedBadge } ${ badgeHtml }
862+ ${ effectiveBadge } ${ badgeHtml }
833863 <span class="item-desc">${ item . category === "mcp" ? "" : esc ( desc ) } </span>
834864 ${ item . category === "mcp" ? "" : `<div class="item-right">
835865 <span class="item-size">${ esc ( sizeLabel ) } </span>
@@ -1046,8 +1076,9 @@ function setupContextBudget() {
10461076 document . getElementById ( "inheritToggleBtn" ) ?. addEventListener ( "click" , ( ) => {
10471077 const scope = getScopeById ( selectedScopeId ) ;
10481078 if ( ! scope || scope . id === "global" ) return ;
1049- showInherited = ! showInherited ;
1050- document . getElementById ( "inheritToggleBtn" ) . classList . toggle ( "active" , showInherited ) ;
1079+ showEffective = ! showEffective ;
1080+ computeEffectiveSets ( selectedScopeId ) ;
1081+ document . getElementById ( "inheritToggleBtn" ) . classList . toggle ( "active" , showEffective ) ;
10511082 renderAll ( ) ;
10521083 } ) ;
10531084}
@@ -2255,10 +2286,49 @@ function findVisibleScopeInTree(scope) {
22552286 return scope . id ;
22562287}
22572288
2289+ /**
2290+ * Pre-compute which global items are shadowed (MCP, agents: project wins same-name)
2291+ * and which items have unresolvable name conflicts (commands).
2292+ * Called whenever showEffective toggles or scope changes.
2293+ */
2294+ function computeEffectiveSets ( scopeId ) {
2295+ effectiveShadowedKeys = new Set ( ) ;
2296+ effectiveConflictKeys = new Set ( ) ;
2297+ if ( ! showEffective || ! scopeId || scopeId === "global" ) return ;
2298+
2299+ const projectItems = getItemsForScope ( scopeId ) ;
2300+ const globalItems = getItemsForScope ( "global" ) ;
2301+
2302+ // MCP & Agents: narrower scope (project) wins same-name
2303+ for ( const cat of [ "mcp" , "agent" ] ) {
2304+ const projectNames = new Set (
2305+ projectItems . filter ( i => i . category === cat ) . map ( i => i . name )
2306+ ) ;
2307+ for ( const gi of globalItems . filter ( i => i . category === cat ) ) {
2308+ if ( projectNames . has ( gi . name ) ) effectiveShadowedKeys . add ( itemKey ( gi ) ) ;
2309+ }
2310+ }
2311+
2312+ // Commands: both levels available but same-name conflicts are officially unsupported
2313+ const projCmdNames = new Set ( projectItems . filter ( i => i . category === "command" ) . map ( i => i . name ) ) ;
2314+ const globalCmdNames = new Set ( globalItems . filter ( i => i . category === "command" ) . map ( i => i . name ) ) ;
2315+ for ( const name of projCmdNames ) {
2316+ if ( ! globalCmdNames . has ( name ) ) continue ;
2317+ for ( const i of [ ...projectItems , ...globalItems ] . filter ( i => i . category === "command" && i . name === name ) ) {
2318+ effectiveConflictKeys . add ( itemKey ( i ) ) ;
2319+ }
2320+ }
2321+ }
2322+
22582323function getVisibleItemsForScope ( scopeId ) {
22592324 const ownItems = getItemsForScope ( scopeId ) . filter ( ( item ) => itemVisibleInMain ( item ) ) ;
2260- if ( ! showInherited || scopeId === "global" ) return ownItems ;
2261- const globalItems = getItemsForScope ( "global" ) . filter ( ( item ) => itemMatchesFilters ( item ) && itemMatchesSearch ( item ) ) ;
2325+ if ( ! showEffective || scopeId === "global" ) return ownItems ;
2326+
2327+ // Only add global items for categories that have an official effective rule
2328+ const globalItems = getItemsForScope ( "global" ) . filter ( ( item ) => {
2329+ if ( ! itemMatchesFilters ( item ) || ! itemMatchesSearch ( item ) ) return false ;
2330+ return Boolean ( CATEGORIES [ item . category ] ?. effectiveRule ) ;
2331+ } ) ;
22622332 return [ ...ownItems , ...globalItems ] ;
22632333}
22642334
0 commit comments