Skip to content

Commit c8234f4

Browse files
ithiria894claude
andcommitted
feat: Show Effective with per-category rules, tree view toggle, detail panel "Why it applies" (v0.13.0)
Major feature additions: - Show Effective: per-category computation using official rules - MCP: project > user dedup, Shadowed badge for overridden servers - Commands: Conflict badge for unsupported same-name conflicts - Agents: project overrides same-name user agents - Memory: project + global + ancestor scope items - Config: ancestor CLAUDE.md files detected from path hierarchy - Skills/Hooks: project + global availability - Categories without official rules (Rule, Plan, Plugin, Session) are dimmed with "No official scope rule" tooltip - Tree view toggle (🌲 button): optional filesystem structure view computes path-based nesting at render time from flat scope data - Detail panel "Why it applies": per-category explanations for every item — Active, Shadowed, Conflict, Ancestor status with human-readable reasoning - Ancestor badge (green): items from parent directory scopes - Remove "inherits from Global" header — misleading since each category has different rules - Scope notice updated: explains per-category rule differences Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c8bdcca commit c8234f4

5 files changed

Lines changed: 219 additions & 36 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@mcpware/claude-code-organizer",
3-
"version": "0.12.0",
3+
"version": "0.13.0",
44
"description": "Organize all your Claude Code memories, skills, MCP servers, commands, agents, rules, and hooks — see what loads globally vs per-project, then move items between scopes",
55
"type": "module",
66
"files": [

src/ui/app.js

Lines changed: 205 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@ let activeFilters = new Set();
1111
let selectedItem = null;
1212
let selectedScopeId = null;
1313
let showEffective = false;
14+
let treeView = false;
1415
// Keys of global items that are shadowed by a project item with the same name
1516
let effectiveShadowedKeys = new Set();
1617
// Keys of items that have a same-name conflict (commands: not reliably overridable)
1718
let effectiveConflictKeys = new Set();
19+
// Keys of items from ancestor scopes (path-based; relevant for CLAUDE.md ancestry)
20+
let effectiveAncestorKeys = new Set();
1821
let pendingDrag = null;
1922
let pendingDelete = null;
2023
let draggingItem = null;
@@ -35,7 +38,7 @@ const CATEGORY_ORDER = ["skill", "memory", "mcp", "command", "agent", "plan", "r
3538

3639
const CATEGORIES = {
3740
memory: { icon: "🧠", label: "Memories", filterLabel: "Memories", group: "memory",
38-
effectiveRule: null }, // ancestry requires runtime cwd — not computed
41+
effectiveRule: "Global memories are available in all projects; project memories are specific to this project" },
3942
skill: { icon: "⚡", label: "Skills", filterLabel: "Skills", group: "skill",
4043
effectiveRule: "Available from Personal (~/.claude/skills), Project (.claude/skills), and installed Plugins" },
4144
session:{ icon: "💬", label: "Sessions", filterLabel: "Sessions", group: null,
@@ -169,7 +172,7 @@ function setupScopeNotice() {
169172
if (!tree) return;
170173
const notice = document.createElement("div");
171174
notice.className = "scope-notice";
172-
notice.innerHTML = `<span class="scope-notice-dismiss" id="scopeNoticeDismiss">✕</span><strong>How scopes work:</strong> Claude Code has two scopes — <strong>Global</strong> and <strong>Project</strong>. Every project inherits directly from Global only. Sibling or nested projects do not inherit from each other.`;
175+
notice.innerHTML = `<span class="scope-notice-dismiss" id="scopeNoticeDismiss">✕</span><strong>How scopes work:</strong> Different categories have different inheritance rules. Use <strong>✦ Show Effective</strong> to see what actually applies in each project. Hover any category pill for its specific rule.`;
173176
tree.parentElement.insertBefore(notice, tree);
174177
document.getElementById("scopeNoticeDismiss").addEventListener("click", () => {
175178
localStorage.setItem(NOTICE_KEY, "1");
@@ -236,7 +239,8 @@ function setupSidebarTree() {
236239
const hdr = event.target.closest(".s-scope-hdr");
237240
if (!hdr) return;
238241
selectedScopeId = hdr.dataset.scopeId;
239-
showEffective = false; effectiveShadowedKeys = new Set(); effectiveConflictKeys = new Set();
242+
showEffective = false; effectiveShadowedKeys = new Set(); effectiveConflictKeys = new Set(); effectiveAncestorKeys = new Set();
243+
document.getElementById("inheritToggleBtn")?.classList.remove("active");
240244
expandScopePath(selectedScopeId);
241245
if (isContextBudgetOpen()) {
242246
openContextBudget(selectedScopeId);
@@ -447,18 +451,26 @@ function setupCollapseAll() {
447451
const button = document.getElementById("collapseAllBtn");
448452
button.addEventListener("click", () => {
449453
if (uiState._dragCollapsed) {
450-
// Restore full view
451454
uiState._dragCollapsed = false;
452455
button.title = "Collapse to tree";
453456
button.textContent = "▤";
454457
} else {
455-
// Collapse: show scope tree only, hide category sub-items
456458
uiState._dragCollapsed = true;
457459
button.title = "Expand all";
458460
button.textContent = "▦";
459461
}
460462
renderSidebar();
461463
});
464+
465+
const treeBtn = document.getElementById("treeViewBtn");
466+
if (treeBtn) {
467+
treeBtn.addEventListener("click", () => {
468+
treeView = !treeView;
469+
treeBtn.classList.toggle("active", treeView);
470+
treeBtn.title = treeView ? "Switch to flat view" : "Switch to tree view (shows filesystem structure)";
471+
renderSidebar();
472+
});
473+
}
462474
}
463475

464476
function updateThemeButton() {
@@ -552,14 +564,61 @@ function renderSidebar() {
552564
return;
553565
}
554566

555-
tree.innerHTML = rootScopes.map((scope) => renderSidebarScope(scope)).join("");
567+
if (treeView) {
568+
// Tree mode: group project scopes by path ancestry for visual hierarchy
569+
// This reflects filesystem structure only — not a universal scope inheritance model
570+
tree.innerHTML = rootScopes.map((scope) => renderSidebarScopeTree(scope)).join("");
571+
} else {
572+
tree.innerHTML = rootScopes.map((scope) => renderSidebarScope(scope)).join("");
573+
}
556574
}
557575

558-
function renderSidebarScope(scope) {
559-
const childHtml = getChildScopes(scope.id)
560-
.filter((child) => scopeVisibleInSidebar(child))
561-
.map((child) => renderSidebarScope(child))
562-
.join("");
576+
/**
577+
* Tree view renderer: computes display-only parent-child relationships from
578+
* filesystem paths. This is a visual grouping only — effective behavior
579+
* depends on each category's own official rules, not this tree.
580+
*/
581+
function renderSidebarScopeTree(scope) {
582+
if (scope.id === "global") {
583+
// Under global, render path-nested projects
584+
const allProjects = (data?.scopes || []).filter(s => s.id !== "global" && scopeVisibleInSidebar(s));
585+
// Sort by path depth then name
586+
allProjects.sort((a, b) => {
587+
const da = (a.repoDir || "").split("/").length;
588+
const db = (b.repoDir || "").split("/").length;
589+
if (da !== db) return da - db;
590+
return (a.name || "").localeCompare(b.name || "");
591+
});
592+
// Find top-level projects (no other project is their path ancestor)
593+
const topLevel = allProjects.filter(s =>
594+
!allProjects.some(other => other.id !== s.id && s.repoDir && other.repoDir && s.repoDir.startsWith(other.repoDir + "/"))
595+
);
596+
const childHtml = topLevel.filter(s => scopeVisibleInSidebar(s)).map(s => renderSidebarScopeTree(s)).join("");
597+
return renderSidebarScope(scope, childHtml);
598+
}
599+
600+
// For project scopes in tree mode: find children by path prefix
601+
const children = (data?.scopes || []).filter(s =>
602+
s.id !== scope.id && s.id !== "global" && s.repoDir && scope.repoDir &&
603+
s.repoDir.startsWith(scope.repoDir + "/") &&
604+
// direct child only: no intermediate scope
605+
!(data?.scopes || []).some(mid =>
606+
mid.id !== s.id && mid.id !== scope.id && mid.id !== "global" &&
607+
mid.repoDir && s.repoDir.startsWith(mid.repoDir + "/") &&
608+
mid.repoDir.startsWith(scope.repoDir + "/")
609+
)
610+
).filter(c => scopeVisibleInSidebar(c));
611+
612+
const childHtml = children.map(c => renderSidebarScopeTree(c)).join("");
613+
return renderSidebarScope(scope, childHtml);
614+
}
615+
616+
function renderSidebarScope(scope, overrideChildHtml) {
617+
const childHtml = overrideChildHtml !== undefined ? overrideChildHtml
618+
: getChildScopes(scope.id)
619+
.filter((child) => scopeVisibleInSidebar(child))
620+
.map((child) => renderSidebarScope(child))
621+
.join("");
563622

564623
const categoryRows = getSidebarCategoryCounts(scope.id)
565624
.map(({ category, count }) => {
@@ -618,21 +677,9 @@ function renderContentHeader() {
618677
title.textContent = scope.name;
619678
tag.textContent = scope.type;
620679

621-
const chain = getScopeChain(scope);
622-
if (chain.length === 0) {
623-
inherit.innerHTML = "";
624-
inherit.style.display = "none";
625-
return;
626-
}
627-
628-
inherit.style.display = "";
629-
inherit.innerHTML = `
630-
<span class="c-inherit-label">inherits from</span>
631-
${chain.map((entry, index) => {
632-
const icon = SCOPE_ICONS[entry.type] || "📂";
633-
const sep = index === chain.length - 1 ? "" : `<span class="c-inherit-sep">›</span>`;
634-
return `<span class="c-inherit-pill">${icon} ${esc(entry.name)}</span>${sep}`;
635-
}).join("")}`;
680+
// No longer show "inherits from Global" — each category has its own rules
681+
inherit.innerHTML = "";
682+
inherit.style.display = "none";
636683
}
637684

638685
function renderPills() {
@@ -822,12 +869,14 @@ function renderItem(item) {
822869
const desc = item.description || item.fileName || item.path || "No description";
823870

824871
// 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>`
872+
const isFromGlobal = showEffective && item.scopeId === "global" && selectedScopeId !== "global";
873+
const isFromAncestor = showEffective && effectiveAncestorKeys.has(key);
874+
const isShadowed = isFromGlobal && effectiveShadowedKeys.has(key);
875+
const isConflict = showEffective && effectiveConflictKeys.has(key);
876+
const effectiveBadge = isShadowed ? `<span class="item-badge ib-shadowed">Shadowed</span>`
877+
: isConflict ? `<span class="item-badge ib-conflict">Conflict</span>`
878+
: isFromAncestor ? `<span class="item-badge ib-ancestor">Ancestor</span>`
879+
: isFromGlobal ? `<span class="item-badge ib-global">Global</span>`
831880
: "";
832881
const actions = (item.locked || isFromGlobal) ? "" : `
833882
<span class="item-actions">
@@ -918,6 +967,9 @@ function renderDetailPanel(resetPreview = false) {
918967
moveBtn.disabled = false; // always enabled — locked items use CC prompt instead of API
919968
deleteBtn.disabled = !canDeleteItem(selectedItem);
920969

970+
// Why it applies (Effective Behavior section)
971+
renderEffectiveBehavior(selectedItem);
972+
921973
// CC Actions — contextual prompt buttons
922974
renderCcActions(selectedItem);
923975

@@ -927,6 +979,89 @@ function renderDetailPanel(resetPreview = false) {
927979
}
928980
}
929981

982+
function renderEffectiveBehavior(item) {
983+
const wrap = document.getElementById("detailEffective");
984+
const text = document.getElementById("detailEffectiveText");
985+
if (!wrap || !text || !item) { wrap?.classList.add("hidden"); return; }
986+
987+
const key = itemKey(item);
988+
const isGlobal = item.scopeId === "global";
989+
const isAncestor = effectiveAncestorKeys.has(key);
990+
const isShadowed = effectiveShadowedKeys.has(key);
991+
const isConflict = effectiveConflictKeys.has(key);
992+
const scope = getScopeById(item.scopeId);
993+
const scopeName = scope?.name || item.scopeId;
994+
995+
let why = "";
996+
997+
switch (item.category) {
998+
case "skill":
999+
why = isGlobal
1000+
? "This skill is installed globally and is available in all projects."
1001+
: "This skill is installed in this project's .claude/skills/ directory.";
1002+
break;
1003+
case "mcp":
1004+
if (isShadowed)
1005+
why = `A project-scoped MCP server with the same name takes precedence over this user-scoped one (rule: local > project > user).`;
1006+
else if (isGlobal)
1007+
why = "This user-scoped MCP server is active for this project. No project-scoped server with the same name was found.";
1008+
else
1009+
why = "This project-scoped MCP server takes precedence over any user-scoped server with the same name (rule: local > project > user).";
1010+
break;
1011+
case "command":
1012+
if (isConflict)
1013+
why = `A command with the same name exists at both user and project level. Claude Code does not guarantee which one applies — same-name conflicts are officially unsupported.`;
1014+
else
1015+
why = isGlobal
1016+
? "This user-level command is globally available."
1017+
: "This command is defined for this project.";
1018+
break;
1019+
case "agent":
1020+
if (isShadowed)
1021+
why = "A project-level agent with the same name overrides this user-level one.";
1022+
else if (isGlobal)
1023+
why = "This user-level agent is available globally. No project-level agent with the same name was found.";
1024+
else
1025+
why = "This project-level agent is available in this project and overrides any user-level agent with the same name.";
1026+
break;
1027+
case "config":
1028+
if (isAncestor)
1029+
why = `This file is in a parent directory of the current project. Claude Code walks up the directory tree from the working directory and loads CLAUDE.md files it finds along the way.`;
1030+
else if (item.name === "CLAUDE.md" || item.name === ".claude/CLAUDE.md")
1031+
why = isGlobal
1032+
? "The global CLAUDE.md is loaded in every Claude Code session."
1033+
: "This project CLAUDE.md is loaded when Claude Code runs in this project.";
1034+
else if (item.name === "settings.local.json")
1035+
why = "Overrides project-shared and user settings for this machine only (not committed to git).";
1036+
else if (item.name === "settings.json")
1037+
why = isGlobal
1038+
? "User-level settings, overridden by project settings.json and settings.local.json."
1039+
: "Project-shared settings, overridden by settings.local.json if present.";
1040+
else
1041+
why = `From ${scopeName} scope.`;
1042+
break;
1043+
case "hook":
1044+
why = isGlobal
1045+
? "This hook is configured globally in user settings."
1046+
: "This hook is configured for this project in project settings files.";
1047+
break;
1048+
case "memory":
1049+
if (isAncestor)
1050+
why = `Stored in a parent project directory (${scopeName}). May be relevant to this project depending on how Claude Code was invoked.`;
1051+
else
1052+
why = isGlobal
1053+
? "This memory is stored globally and is accessible in all projects."
1054+
: "This memory is stored in this project's memory directory.";
1055+
break;
1056+
default:
1057+
why = CATEGORIES[item.category]?.effectiveRule || "";
1058+
}
1059+
1060+
if (!why) { wrap.classList.add("hidden"); return; }
1061+
wrap.classList.remove("hidden");
1062+
text.textContent = why;
1063+
}
1064+
9301065
function renderCcActions(item) {
9311066
const container = document.getElementById("detailCcActions");
9321067
const btnRow = document.getElementById("ccBtnRow");
@@ -2291,9 +2426,25 @@ function findVisibleScopeInTree(scope) {
22912426
* and which items have unresolvable name conflicts (commands).
22922427
* Called whenever showEffective toggles or scope changes.
22932428
*/
2429+
/**
2430+
* Returns scopes whose repoDir is a path ancestor of the given scope's repoDir.
2431+
* e.g. if scopeId is /home/user/company/repo, returns the scope for /home/user/company if it exists.
2432+
*/
2433+
function getAncestorScopes(scopeId) {
2434+
const scope = getScopeById(scopeId);
2435+
if (!scope?.repoDir) return [];
2436+
return (data?.scopes || []).filter(s =>
2437+
s.repoDir &&
2438+
s.id !== scopeId &&
2439+
s.id !== "global" &&
2440+
scope.repoDir.startsWith(s.repoDir + "/")
2441+
);
2442+
}
2443+
22942444
function computeEffectiveSets(scopeId) {
22952445
effectiveShadowedKeys = new Set();
22962446
effectiveConflictKeys = new Set();
2447+
effectiveAncestorKeys = new Set();
22972448
if (!showEffective || !scopeId || scopeId === "global") return;
22982449

22992450
const projectItems = getItemsForScope(scopeId);
@@ -2318,6 +2469,15 @@ function computeEffectiveSets(scopeId) {
23182469
effectiveConflictKeys.add(itemKey(i));
23192470
}
23202471
}
2472+
2473+
// Ancestor scopes: scopes whose repoDir is a path parent of this project.
2474+
// Their config items (especially CLAUDE.md) are ancestor-loaded by Claude Code.
2475+
const ancestorScopes = getAncestorScopes(scopeId);
2476+
for (const as of ancestorScopes) {
2477+
for (const i of getItemsForScope(as.id).filter(i => i.category === "config" || i.category === "memory")) {
2478+
effectiveAncestorKeys.add(itemKey(i));
2479+
}
2480+
}
23212481
}
23222482

23232483
function getVisibleItemsForScope(scopeId) {
@@ -2329,7 +2489,19 @@ function getVisibleItemsForScope(scopeId) {
23292489
if (!itemMatchesFilters(item) || !itemMatchesSearch(item)) return false;
23302490
return Boolean(CATEGORIES[item.category]?.effectiveRule);
23312491
});
2332-
return [...ownItems, ...globalItems];
2492+
2493+
// Add ancestor scope items (CLAUDE.md + memories from path-parent projects)
2494+
const ancestorItems = [];
2495+
for (const as of getAncestorScopes(scopeId)) {
2496+
for (const item of getItemsForScope(as.id)) {
2497+
if ((item.category === "config" || item.category === "memory") &&
2498+
itemMatchesFilters(item) && itemMatchesSearch(item)) {
2499+
ancestorItems.push(item);
2500+
}
2501+
}
2502+
}
2503+
2504+
return [...ownItems, ...globalItems, ...ancestorItems];
23332505
}
23342506

23352507
function itemVisibleInMain(item) {

src/ui/index.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
</div>
1818
<span class="sidebar-title">Claude Code Organizer</span>
1919
<span style="flex:1"></span>
20+
<button class="theme-btn" id="treeViewBtn" type="button" title="Toggle tree view (shows filesystem structure)">🌲</button>
2021
<button class="theme-btn" id="collapseAllBtn" type="button" title="Collapse all scopes"></button>
2122
<button class="theme-btn" id="themeToggle" type="button"></button>
2223
</div>
@@ -84,6 +85,11 @@ <h2 id="detailTitle">Select an item</h2>
8485
<div class="d-desc" id="detailDesc">Select an item to inspect its metadata and preview.</div>
8586
</div>
8687

88+
<div class="d-effective-wrap hidden" id="detailEffective">
89+
<span class="d-info-label">Why it applies</span>
90+
<div class="d-effective" id="detailEffectiveText"></div>
91+
</div>
92+
8793
<div class="d-info-grid" id="detailDates">
8894
<div class="d-info-cell">
8995
<span class="d-info-label">Created</span>

0 commit comments

Comments
 (0)