This repository was archived by the owner on May 15, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
feat: add skills management UI to settings panel (#10513) #10844
Merged
Merged
Changes from 7 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
1834888
feat: add skills management UI to settings panel (#10513)
f09c06e
fix: rename unused onValueChange parameters in test mocks
abd504e
fix: address PR review feedback
b808cd3
feat(i18n): add skills translations for all 17 languages
58d5967
fix: use sentinel value for 'Any mode' SelectItem to fix Radix UI error
daniel-lxs e044de2
refactor: extract skills message handling into dedicated handler
daniel-lxs 1a2cae6
refactor: improve code quality and add i18n for skills feature
daniel-lxs dda3b71
fix: remove unrelated mergeWorktree references from types
roomote File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| import { | ||
| validateSkillName, | ||
| SkillNameValidationError, | ||
| SKILL_NAME_MIN_LENGTH, | ||
| SKILL_NAME_MAX_LENGTH, | ||
| SKILL_NAME_REGEX, | ||
| } from "../skills.js" | ||
|
|
||
| describe("validateSkillName", () => { | ||
| describe("valid names", () => { | ||
| it("accepts single lowercase word", () => { | ||
| expect(validateSkillName("myskill")).toEqual({ valid: true }) | ||
| }) | ||
|
|
||
| it("accepts lowercase letters and numbers", () => { | ||
| expect(validateSkillName("skill123")).toEqual({ valid: true }) | ||
| }) | ||
|
|
||
| it("accepts hyphenated words", () => { | ||
| expect(validateSkillName("my-skill")).toEqual({ valid: true }) | ||
| }) | ||
|
|
||
| it("accepts multiple hyphenated words", () => { | ||
| expect(validateSkillName("my-awesome-skill")).toEqual({ valid: true }) | ||
| }) | ||
|
|
||
| it("accepts single character", () => { | ||
| expect(validateSkillName("a")).toEqual({ valid: true }) | ||
| }) | ||
|
|
||
| it("accepts single digit", () => { | ||
| expect(validateSkillName("1")).toEqual({ valid: true }) | ||
| }) | ||
|
|
||
| it("accepts maximum length name (64 characters)", () => { | ||
| const maxLengthName = "a".repeat(SKILL_NAME_MAX_LENGTH) | ||
| expect(validateSkillName(maxLengthName)).toEqual({ valid: true }) | ||
| }) | ||
| }) | ||
|
|
||
| describe("empty or missing names", () => { | ||
| it("rejects empty string", () => { | ||
| expect(validateSkillName("")).toEqual({ | ||
| valid: false, | ||
| error: SkillNameValidationError.Empty, | ||
| }) | ||
| }) | ||
| }) | ||
|
|
||
| describe("names that are too long", () => { | ||
| it("rejects names longer than 64 characters", () => { | ||
| const tooLongName = "a".repeat(SKILL_NAME_MAX_LENGTH + 1) | ||
| expect(validateSkillName(tooLongName)).toEqual({ | ||
| valid: false, | ||
| error: SkillNameValidationError.TooLong, | ||
| }) | ||
| }) | ||
| }) | ||
|
|
||
| describe("invalid format", () => { | ||
| it("rejects uppercase letters", () => { | ||
| expect(validateSkillName("MySkill")).toEqual({ | ||
| valid: false, | ||
| error: SkillNameValidationError.InvalidFormat, | ||
| }) | ||
| }) | ||
|
|
||
| it("rejects leading hyphen", () => { | ||
| expect(validateSkillName("-myskill")).toEqual({ | ||
| valid: false, | ||
| error: SkillNameValidationError.InvalidFormat, | ||
| }) | ||
| }) | ||
|
|
||
| it("rejects trailing hyphen", () => { | ||
| expect(validateSkillName("myskill-")).toEqual({ | ||
| valid: false, | ||
| error: SkillNameValidationError.InvalidFormat, | ||
| }) | ||
| }) | ||
|
|
||
| it("rejects consecutive hyphens", () => { | ||
| expect(validateSkillName("my--skill")).toEqual({ | ||
| valid: false, | ||
| error: SkillNameValidationError.InvalidFormat, | ||
| }) | ||
| }) | ||
|
|
||
| it("rejects spaces", () => { | ||
| expect(validateSkillName("my skill")).toEqual({ | ||
| valid: false, | ||
| error: SkillNameValidationError.InvalidFormat, | ||
| }) | ||
| }) | ||
|
|
||
| it("rejects underscores", () => { | ||
| expect(validateSkillName("my_skill")).toEqual({ | ||
| valid: false, | ||
| error: SkillNameValidationError.InvalidFormat, | ||
| }) | ||
| }) | ||
|
|
||
| it("rejects special characters", () => { | ||
| expect(validateSkillName("my@skill")).toEqual({ | ||
| valid: false, | ||
| error: SkillNameValidationError.InvalidFormat, | ||
| }) | ||
| }) | ||
|
|
||
| it("rejects dots", () => { | ||
| expect(validateSkillName("my.skill")).toEqual({ | ||
| valid: false, | ||
| error: SkillNameValidationError.InvalidFormat, | ||
| }) | ||
| }) | ||
| }) | ||
| }) | ||
|
|
||
| describe("SKILL_NAME_REGEX", () => { | ||
| it("matches valid names", () => { | ||
| expect(SKILL_NAME_REGEX.test("myskill")).toBe(true) | ||
| expect(SKILL_NAME_REGEX.test("my-skill")).toBe(true) | ||
| expect(SKILL_NAME_REGEX.test("skill123")).toBe(true) | ||
| expect(SKILL_NAME_REGEX.test("a1-b2-c3")).toBe(true) | ||
| }) | ||
|
|
||
| it("does not match invalid names", () => { | ||
| expect(SKILL_NAME_REGEX.test("-start")).toBe(false) | ||
| expect(SKILL_NAME_REGEX.test("end-")).toBe(false) | ||
| expect(SKILL_NAME_REGEX.test("double--hyphen")).toBe(false) | ||
| expect(SKILL_NAME_REGEX.test("UPPER")).toBe(false) | ||
| expect(SKILL_NAME_REGEX.test("")).toBe(false) | ||
| }) | ||
| }) | ||
|
|
||
| describe("constants", () => { | ||
| it("has correct min length", () => { | ||
| expect(SKILL_NAME_MIN_LENGTH).toBe(1) | ||
| }) | ||
|
|
||
| it("has correct max length", () => { | ||
| expect(SKILL_NAME_MAX_LENGTH).toBe(64) | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| /** | ||
| * Skill metadata for discovery (loaded at startup) | ||
| * Only name and description are required for now | ||
| */ | ||
| export interface SkillMetadata { | ||
| name: string // Required: skill identifier | ||
| description: string // Required: when to use this skill | ||
| path: string // Absolute path to SKILL.md | ||
| source: "global" | "project" // Where the skill was discovered | ||
| mode?: string // If set, skill is only available in this mode | ||
| } | ||
|
|
||
| /** | ||
| * Skill name validation constants per agentskills.io specification: | ||
| * https://agentskills.io/specification | ||
| * | ||
| * Name constraints: | ||
| * - 1-64 characters | ||
| * - Lowercase letters, numbers, and hyphens only | ||
| * - Must not start or end with a hyphen | ||
| * - Must not contain consecutive hyphens | ||
| */ | ||
| export const SKILL_NAME_MIN_LENGTH = 1 | ||
| export const SKILL_NAME_MAX_LENGTH = 64 | ||
|
|
||
| /** | ||
| * Regex pattern for valid skill names. | ||
| * Matches: lowercase letters/numbers, optionally followed by groups of hyphen + lowercase letters/numbers. | ||
| * This ensures no leading/trailing hyphens and no consecutive hyphens. | ||
| */ | ||
| export const SKILL_NAME_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/ | ||
|
|
||
| /** | ||
| * Error codes for skill name validation. | ||
| * These can be mapped to translation keys in the frontend or error messages in the backend. | ||
| */ | ||
| export enum SkillNameValidationError { | ||
| Empty = "empty", | ||
| TooLong = "too_long", | ||
| InvalidFormat = "invalid_format", | ||
| } | ||
|
|
||
| /** | ||
| * Result of skill name validation. | ||
| */ | ||
| export interface SkillNameValidationResult { | ||
| valid: boolean | ||
| error?: SkillNameValidationError | ||
| } | ||
|
|
||
| /** | ||
| * Validate a skill name according to agentskills.io specification. | ||
| * | ||
| * @param name - The skill name to validate | ||
| * @returns Validation result with error code if invalid | ||
| */ | ||
| export function validateSkillName(name: string): SkillNameValidationResult { | ||
| if (!name || name.length < SKILL_NAME_MIN_LENGTH) { | ||
| return { valid: false, error: SkillNameValidationError.Empty } | ||
| } | ||
|
|
||
| if (name.length > SKILL_NAME_MAX_LENGTH) { | ||
| return { valid: false, error: SkillNameValidationError.TooLong } | ||
| } | ||
|
|
||
| if (!SKILL_NAME_REGEX.test(name)) { | ||
| return { valid: false, error: SkillNameValidationError.InvalidFormat } | ||
| } | ||
|
|
||
| return { valid: true } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this unrelated / can we remove?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@roomote remove references to mergeWorktreeResult
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed unrelated
mergeWorktreeResultandmergeWorktreereferences from the types file. All checks passed.View commit | Revert commit