From 633429303551586fa51f766d92fc2310ca39a578 Mon Sep 17 00:00:00 2001 From: Ttungx <1323593614@qq.com> Date: Fri, 14 Aug 2026 15:34:34 +0800 Subject: [PATCH] fix(ui): normalize Windows paths in skill visibility filtering --- .../ui/src/stores/skillVisibility.test.ts | 28 +++++++++++++++++++ packages/ui/src/stores/skillVisibility.ts | 8 ++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/stores/skillVisibility.test.ts b/packages/ui/src/stores/skillVisibility.test.ts index 1aa9f353..c4470fc5 100644 --- a/packages/ui/src/stores/skillVisibility.test.ts +++ b/packages/ui/src/stores/skillVisibility.test.ts @@ -6,6 +6,9 @@ const skill = (name: string, path: string) => ({ name, path }); const AGENTS = (name: string) => skill(name, `/repo/.agents/skills/${name}/SKILL.md`); const CLAUDE = (name: string) => skill(name, `/repo/.claude/skills/${name}/SKILL.md`); const OPENCODE = (name: string) => skill(name, `/home/u/.config/opencode/skill/${name}/SKILL.md`); +const WIN_AGENTS = (name: string) => skill(name, String.raw`C:\Users\u\.agents\skills\${name}\SKILL.md`); +const WIN_CLAUDE = (name: string) => skill(name, String.raw`C:\Users\u\.claude\skills\${name}\SKILL.md`); +const WIN_OPENCODE = (name: string) => skill(name, String.raw`C:\Users\u\.config\opencode\skill\${name}\SKILL.md`); const ENABLED = { claudeDisabled: false, allDisabled: false }; @@ -20,6 +23,13 @@ describe('resolveSkillRoot', () => { test('does not match a directory that merely contains the name', () => { expect(resolveSkillRoot('/repo/my.claude.backup/skills/a/SKILL.md')).toBe('opencode'); }); + + test('classifies Windows backslash paths', () => { + expect(resolveSkillRoot(WIN_CLAUDE('a').path)).toBe('claude'); + expect(resolveSkillRoot(WIN_AGENTS('a').path)).toBe('agents'); + expect(resolveSkillRoot(WIN_OPENCODE('a').path)).toBe('opencode'); + expect(resolveSkillRoot(String.raw`C:\repo\my.claude.backup\skills\a\SKILL.md`)).toBe('opencode'); + }); }); describe('filterSkillsByRuntimeFlags', () => { @@ -72,4 +82,22 @@ describe('filterSkillsByRuntimeFlags', () => { const result = filterSkillsByRuntimeFlags([CLAUDE('only-claude'), AGENTS('other')], ENABLED); expect(result.map((s) => s.name).sort()).toEqual(['only-claude', 'other']); }); + + test('drops Windows .agents and .claude skills when external skills are disabled', () => { + const skills = [WIN_AGENTS('a'), WIN_CLAUDE('b'), WIN_OPENCODE('c')]; + const result = filterSkillsByRuntimeFlags(skills, { claudeDisabled: false, allDisabled: true }); + expect(result.map((s) => s.name)).toEqual(['c']); + }); + + test('drops only Windows .claude skills when claude skills are disabled', () => { + const skills = [WIN_AGENTS('a'), WIN_CLAUDE('b'), WIN_OPENCODE('c')]; + const result = filterSkillsByRuntimeFlags(skills, { claudeDisabled: true, allDisabled: false }); + expect(result.map((s) => s.name).sort()).toEqual(['a', 'c']); + }); + + test('prefers the .agents copy for a duplicated name on Windows', () => { + const result = filterSkillsByRuntimeFlags([WIN_CLAUDE('dup'), WIN_AGENTS('dup')], ENABLED); + expect(result).toHaveLength(1); + expect(result[0].path).toContain('.agents'); + }); }); diff --git a/packages/ui/src/stores/skillVisibility.ts b/packages/ui/src/stores/skillVisibility.ts index ebb28377..da321c27 100644 --- a/packages/ui/src/stores/skillVisibility.ts +++ b/packages/ui/src/stores/skillVisibility.ts @@ -35,8 +35,12 @@ const AGENTS_ROOT = /(^|\/)\.agents\//; type SkillRoot = 'claude' | 'agents' | 'opencode'; export const resolveSkillRoot = (skillPath: string): SkillRoot => { - if (CLAUDE_ROOT.test(skillPath)) return 'claude'; - if (AGENTS_ROOT.test(skillPath)) return 'agents'; + // Server discovery joins paths with the platform separator, so Windows + // skill paths arrive with backslashes. Normalize before matching the + // root regexes, which are expressed with forward slashes. + const normalized = skillPath.replace(/\\/g, '/'); + if (CLAUDE_ROOT.test(normalized)) return 'claude'; + if (AGENTS_ROOT.test(normalized)) return 'agents'; return 'opencode'; };