Skip to content

Commit c8bdcca

Browse files
ithiria894claude
andcommitted
feat: Show Effective mode with per-category official scope rules (v0.12.0)
Replaces "Show Inherited" with "Show Effective". When toggled: - Only categories with official Claude Code scope rules participate (skill, mcp, command, agent, config, hook) - Categories without a clear scope rule (memory, rule, plan, plugin, session) are dimmed with tooltip "No official scope rule — shown as inventory only" - MCP: deduplicates by name using project > user precedence; shadowed servers get a "Shadowed" badge - Commands: flags same-name conflicts with a "Conflict" badge (official docs say same-name conflicts are not supported) - Agents: project-level agents shadow same-name user agents - All participating category pills show their official rule on hover Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 46850ed commit c8bdcca

5 files changed

Lines changed: 104 additions & 30 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.11.9",
3+
"version": "0.12.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: 96 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ let data = null;
1010
let activeFilters = new Set();
1111
let selectedItem = null;
1212
let 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();
1418
let pendingDrag = null;
1519
let pendingDelete = null;
1620
let draggingItem = null;
@@ -30,17 +34,28 @@ const uiState = {
3034
const CATEGORY_ORDER = ["skill", "memory", "mcp", "command", "agent", "plan", "rule", "config", "hook", "plugin", "session"];
3135

3236
const 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

4661
const 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

623638
function 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+
22582323
function 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

src/ui/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
<span class="content-title" id="contentTitle">Organizer</span>
4242
<span class="content-scope-tag" id="contentScopeTag">global</span>
4343
<div class="content-inherit" id="contentInherit"></div>
44-
<button class="export-btn inherit-toggle-btn" id="inheritToggleBtn" type="button" title="Show items inherited from Global scope">🌐 Show Inherited</button>
44+
<button class="export-btn inherit-toggle-btn" id="inheritToggleBtn" type="button" title="Show items resolved by each category's official scope rules"> Show Effective</button>
4545
<button class="export-btn ctx-budget-btn" id="ctxBudgetBtn" type="button" title="Show context token budget for this scope">Context Budget</button>
4646
</div>
4747
<div class="filter-bar" id="pills"></div>

src/ui/style.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,10 @@ body {
949949
.ctx-budget-btn:hover { border-color: var(--accent); color: var(--accent-text); background: var(--accent-light); }
950950
.ib-global { color: var(--badge-user); }
951951
.ib-global::before { background: var(--badge-user); }
952+
.ib-shadowed { color: var(--text-muted); opacity: 0.7; }
953+
.ib-shadowed::before { background: var(--text-muted); }
954+
.ib-conflict { color: oklch(0.65 0.15 55); }
955+
.ib-conflict::before { background: oklch(0.65 0.15 55); }
952956

953957
.ctx-budget-summary { padding: 16px 20px; border-bottom: 1px solid var(--border-light); }
954958
.ctx-budget-bar-wrap {

0 commit comments

Comments
 (0)