Browse Source

feat(marketplace): add Skills tab with install/remove support

Add Skills as a third marketplace category alongside Modes and MCP Servers.
This enables users to browse, install, and remove skills from the marketplace.

Backend changes:
- Add 'skill' type to MarketplaceItem discriminated union
- Add SkillMarketplaceItem schema with Zod validation
- Extend RemoteConfigLoader to fetch skills from API
- Update SimpleInstaller to handle skill install/remove operations
- Add SKILLS_MARKETPLACE_ENABLED feature flag

Frontend changes:
- Add Skills tab to MarketplaceView (3-tab layout)
- Create SkillsMarketplace component with search and category filtering
- Create SkillItemCard component for displaying skill details
- Update MarketplaceInstallModal to handle skill items
- Add translations for skills marketplace in 21 languages
Mark IJbema 1 month ago
parent
commit
91a5ae0789
58 changed files with 996 additions and 47 deletions
  1. 6 0
      packages/types/src/feature-flags.ts
  2. 1 0
      packages/types/src/index.ts
  3. 4 1
      packages/types/src/marketplace.ts
  4. 65 0
      packages/types/src/skill.ts
  5. 49 5
      src/services/marketplace/RemoteConfigLoader.ts
  6. 31 16
      src/services/marketplace/SimpleInstaller.ts
  7. 320 0
      src/services/marketplace/__tests__/RemoteConfigLoader.skill.spec.ts
  8. 44 9
      src/services/marketplace/__tests__/RemoteConfigLoader.spec.ts
  9. 1 1
      src/shared/WebviewMessage.ts
  10. 36 7
      webview-ui/src/components/marketplace/MarketplaceView.tsx
  11. 9 3
      webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts
  12. 115 0
      webview-ui/src/components/marketplace/SkillsMarketplace.tsx
  13. 12 5
      webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx
  14. 61 0
      webview-ui/src/components/marketplace/components/SkillItemCard.tsx
  15. 3 0
      webview-ui/src/i18n/locales/ar/kilocode.json
  16. 8 0
      webview-ui/src/i18n/locales/ar/marketplace.json
  17. 3 0
      webview-ui/src/i18n/locales/ca/kilocode.json
  18. 8 0
      webview-ui/src/i18n/locales/ca/marketplace.json
  19. 3 0
      webview-ui/src/i18n/locales/cs/kilocode.json
  20. 8 0
      webview-ui/src/i18n/locales/cs/marketplace.json
  21. 3 0
      webview-ui/src/i18n/locales/de/kilocode.json
  22. 8 0
      webview-ui/src/i18n/locales/de/marketplace.json
  23. 3 0
      webview-ui/src/i18n/locales/en/kilocode.json
  24. 8 0
      webview-ui/src/i18n/locales/en/marketplace.json
  25. 3 0
      webview-ui/src/i18n/locales/es/kilocode.json
  26. 8 0
      webview-ui/src/i18n/locales/es/marketplace.json
  27. 3 0
      webview-ui/src/i18n/locales/fr/kilocode.json
  28. 8 0
      webview-ui/src/i18n/locales/fr/marketplace.json
  29. 3 0
      webview-ui/src/i18n/locales/hi/kilocode.json
  30. 8 0
      webview-ui/src/i18n/locales/hi/marketplace.json
  31. 3 0
      webview-ui/src/i18n/locales/id/kilocode.json
  32. 8 0
      webview-ui/src/i18n/locales/id/marketplace.json
  33. 3 0
      webview-ui/src/i18n/locales/it/kilocode.json
  34. 8 0
      webview-ui/src/i18n/locales/it/marketplace.json
  35. 3 0
      webview-ui/src/i18n/locales/ja/kilocode.json
  36. 8 0
      webview-ui/src/i18n/locales/ja/marketplace.json
  37. 3 0
      webview-ui/src/i18n/locales/ko/kilocode.json
  38. 8 0
      webview-ui/src/i18n/locales/ko/marketplace.json
  39. 3 0
      webview-ui/src/i18n/locales/nl/kilocode.json
  40. 8 0
      webview-ui/src/i18n/locales/nl/marketplace.json
  41. 3 0
      webview-ui/src/i18n/locales/pl/kilocode.json
  42. 8 0
      webview-ui/src/i18n/locales/pl/marketplace.json
  43. 3 0
      webview-ui/src/i18n/locales/pt-BR/kilocode.json
  44. 8 0
      webview-ui/src/i18n/locales/pt-BR/marketplace.json
  45. 3 0
      webview-ui/src/i18n/locales/ru/kilocode.json
  46. 8 0
      webview-ui/src/i18n/locales/ru/marketplace.json
  47. 3 0
      webview-ui/src/i18n/locales/th/kilocode.json
  48. 8 0
      webview-ui/src/i18n/locales/th/marketplace.json
  49. 3 0
      webview-ui/src/i18n/locales/tr/kilocode.json
  50. 8 0
      webview-ui/src/i18n/locales/tr/marketplace.json
  51. 3 0
      webview-ui/src/i18n/locales/uk/kilocode.json
  52. 8 0
      webview-ui/src/i18n/locales/uk/marketplace.json
  53. 3 0
      webview-ui/src/i18n/locales/vi/kilocode.json
  54. 8 0
      webview-ui/src/i18n/locales/vi/marketplace.json
  55. 3 0
      webview-ui/src/i18n/locales/zh-CN/kilocode.json
  56. 8 0
      webview-ui/src/i18n/locales/zh-CN/marketplace.json
  57. 3 0
      webview-ui/src/i18n/locales/zh-TW/kilocode.json
  58. 8 0
      webview-ui/src/i18n/locales/zh-TW/marketplace.json

+ 6 - 0
packages/types/src/feature-flags.ts

@@ -12,3 +12,9 @@ export const MODEL_SELECTION_ENABLED = process.env.NODE_ENV === "development"
  * Enable extreme snooze values for autocomplete in development mode.
  */
 export const EXTREME_SNOOZE_VALUES_ENABLED = process.env.NODE_ENV === "development"
+
+/**
+ * Enable skills marketplace tab in development mode.
+ * This allows developers to test the skills marketplace feature before it's ready for production.
+ */
+export const SKILLS_MARKETPLACE_ENABLED = process.env.NODE_ENV === "development"

+ 1 - 0
packages/types/src/index.ts

@@ -14,6 +14,7 @@ export * from "./global-settings.js"
 export * from "./history.js"
 export * from "./image-generation.js"
 export * from "./ipc.js"
+export * from "./skill.js" // kilocode_change
 export * from "./marketplace.js"
 export * from "./mcp.js"
 export * from "./message.js"

+ 4 - 1
packages/types/src/marketplace.ts

@@ -1,4 +1,5 @@
 import { z } from "zod"
+import { skillMarketplaceItemSchema } from "./skill.js" // kilocode_change
 
 /**
  * Schema for MCP parameter definitions
@@ -27,7 +28,7 @@ export type McpInstallationMethod = z.infer<typeof mcpInstallationMethodSchema>
 /**
  * Component type validation
  */
-export const marketplaceItemTypeSchema = z.enum(["mode", "mcp"] as const)
+export const marketplaceItemTypeSchema = z.enum(["mode", "mcp", "skill"] as const) // kilocode_change: Added "skill"
 
 export type MarketplaceItemType = z.infer<typeof marketplaceItemTypeSchema>
 
@@ -73,6 +74,8 @@ export const marketplaceItemSchema = z.discriminatedUnion("type", [
 	mcpMarketplaceItemSchema.extend({
 		type: z.literal("mcp"),
 	}),
+	// kilocode_change: Skill marketplace item
+	skillMarketplaceItemSchema,
 ])
 
 export type MarketplaceItem = z.infer<typeof marketplaceItemSchema>

+ 65 - 0
packages/types/src/skill.ts

@@ -0,0 +1,65 @@
+// kilocode_change new file
+
+import { z } from "zod"
+
+/**
+ * Type definitions for Skills Marketplace items
+ *
+ * These types define the structure of skills data that will be served
+ * from the Skills Marketplace API and displayed in the UI.
+ */
+
+/**
+ * Schema for raw skill data from the API (before transformation to MarketplaceItem)
+ */
+export const rawSkillSchema = z.object({
+	// Core identity
+	id: z.string(),
+	description: z.string(),
+
+	// Categorization
+	category: z.string(),
+
+	// URLs for viewing/downloading
+	githubUrl: z.string(),
+	rawUrl: z.string(),
+})
+
+export type RawSkill = z.infer<typeof rawSkillSchema>
+
+/**
+ * A skill as a marketplace item (extends base marketplace item schema)
+ * This is defined in marketplace.ts as part of the discriminated union
+ */
+export const skillMarketplaceItemSchema = z.object({
+	type: z.literal("skill"),
+	id: z.string().min(1),
+	name: z.string().min(1),
+	description: z.string(),
+	author: z.string().optional(),
+	authorUrl: z.string().url("Author URL must be a valid URL").optional(),
+	tags: z.array(z.string()).optional(),
+	prerequisites: z.array(z.string()).optional(),
+	// Skill-specific fields
+	category: z.string(),
+	githubUrl: z.string(),
+	rawUrl: z.string(),
+	// Display fields (computed from id and category)
+	displayName: z.string(),
+	displayCategory: z.string(),
+})
+
+export type SkillMarketplaceItem = z.infer<typeof skillMarketplaceItemSchema>
+
+export function isSkillItem(item: { type: string }): item is SkillMarketplaceItem {
+	return item.type === "skill"
+}
+
+/**
+ * Container for raw skills from the API (the YAML output format from backend)
+ */
+export const skillsMarketplaceCatalogSchema = z.object({
+	items: z.array(rawSkillSchema),
+})
+
+export type SkillsMarketplaceCatalog = z.infer<typeof skillsMarketplaceCatalogSchema>

+ 49 - 5
src/services/marketplace/RemoteConfigLoader.ts

@@ -5,8 +5,10 @@ import { getApiUrl } from "@roo-code/types" // kilocode_change
 import {
 	type MarketplaceItem,
 	type MarketplaceItemType,
+	type SkillMarketplaceItem, // kilocode_change
 	modeMarketplaceItemSchema,
 	mcpMarketplaceItemSchema,
+	skillsMarketplaceCatalogSchema, // kilocode_change
 } from "@roo-code/types"
 //import { getRooCodeApiUrl } from "@roo-code/cloud" kilocode_change: use our own api
 
@@ -29,15 +31,15 @@ export class RemoteConfigLoader {
 	// }
 
 	async loadAllItems(hideMarketplaceMcps = false): Promise<MarketplaceItem[]> {
-		const items: MarketplaceItem[] = []
-
 		const modesPromise = this.fetchModes()
 		const mcpsPromise = hideMarketplaceMcps ? Promise.resolve([]) : this.fetchMcps()
+		// kilocode_change start - add skills
+		const skillsPromise = this.fetchSkills()
 
-		const [modes, mcps] = await Promise.all([modesPromise, mcpsPromise])
+		const [modes, mcps, skills] = await Promise.all([modesPromise, mcpsPromise, skillsPromise])
 
-		items.push(...modes, ...mcps)
-		return items
+		return [...modes, ...mcps, ...skills]
+		//kilocode change end
 	}
 
 	private async fetchModes(): Promise<MarketplaceItem[]> {
@@ -86,6 +88,48 @@ export class RemoteConfigLoader {
 		return items
 	}
 
+	// kilocode_change start - fetch skills from marketplace API and transform to MarketplaceItem
+	private async fetchSkills(): Promise<MarketplaceItem[]> {
+		const cacheKey = "skills"
+		const cached = this.getFromCache(cacheKey)
+
+		if (cached) {
+			return cached
+		}
+
+		// Convert kebab-case to Title Case (e.g., "my-skill" -> "My Skill")
+		const kebabToTitleCase = (str: string): string =>
+			str
+				.split("-")
+				.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
+				.join(" ")
+
+		const url = getApiUrl("/api/marketplace/skills")
+		const data = await this.fetchWithRetry<string>(url)
+
+		const yamlData = yaml.parse(data)
+		const validated = skillsMarketplaceCatalogSchema.parse(yamlData)
+
+		// Transform raw skills to MarketplaceItem format
+		const items: MarketplaceItem[] = validated.items.map(
+			(rawSkill): SkillMarketplaceItem => ({
+				type: "skill" as const,
+				id: rawSkill.id,
+				name: rawSkill.id, // Use id as name (UI derives display name from id)
+				description: rawSkill.description,
+				category: rawSkill.category,
+				githubUrl: rawSkill.githubUrl,
+				rawUrl: rawSkill.rawUrl,
+				displayName: kebabToTitleCase(rawSkill.id),
+				displayCategory: kebabToTitleCase(rawSkill.category),
+			}),
+		)
+
+		this.setCache(cacheKey, items)
+		return items
+	}
+	// kilocode_change end
+
 	private async fetchWithRetry<T>(url: string, maxRetries = 3): Promise<T> {
 		let lastError: Error
 

+ 31 - 16
src/services/marketplace/SimpleInstaller.ts

@@ -26,15 +26,22 @@ export class SimpleInstaller {
 				return await this.installMode(item, target)
 			case "mcp":
 				return await this.installMcp(item, target, options)
+			// kilocode_change start - Handle skill type
+			case "skill":
+				// Skills are not installable through the marketplace - they are managed separately
+				throw new Error("Skills cannot be installed through the marketplace installer")
+			// kilocode_change end
 			default:
 				throw new Error(`Unsupported item type: ${(item as any).type}`)
 		}
 	}
 
+	// kilocode_change start - Use narrowed type for better type safety
 	private async installMode(
-		item: MarketplaceItem,
+		item: Extract<MarketplaceItem, { type: "mode" }>,
 		target: "project" | "global",
 	): Promise<{ filePath: string; line?: number }> {
+		// kilocode_change end
 		if (!item.content) {
 			throw new Error("Mode item missing content")
 		}
@@ -154,11 +161,13 @@ export class SimpleInstaller {
 		return { filePath, line }
 	}
 
+	// kilocode_change start - Use narrowed type for better type safety
 	private async installMcp(
-		item: MarketplaceItem,
+		item: Extract<MarketplaceItem, { type: "mcp" }>,
 		target: "project" | "global",
 		options?: InstallOptions,
 	): Promise<{ filePath: string; line?: number }> {
+		// kilocode_change end
 		if (!item.content) {
 			throw new Error("MCP item missing content")
 		}
@@ -183,7 +192,7 @@ export class SimpleInstaller {
 		}
 
 		// Merge parameters (method-specific override global)
-		const itemParameters = item.type === "mcp" ? item.parameters || [] : []
+		const itemParameters = item.parameters || [] // kilocode_change - simplified due to narrowed type
 		const allParameters = [...itemParameters, ...methodParameters]
 		const uniqueParameters = Array.from(new Map(allParameters.map((p) => [p.key, p])).values())
 
@@ -207,7 +216,7 @@ export class SimpleInstaller {
 				methodParameters = method.parameters || []
 
 				// Re-merge parameters with the newly selected method
-				const itemParametersForNewMethod = item.type === "mcp" ? item.parameters || [] : []
+				const itemParametersForNewMethod = item.parameters || [] // kilocode_change - simplified due to narrowed type
 				const allParametersForNewMethod = [...itemParametersForNewMethod, ...methodParameters]
 				const uniqueParametersForNewMethod = Array.from(
 					new Map(allParametersForNewMethod.map((p) => [p.key, p])).values(),
@@ -288,12 +297,22 @@ export class SimpleInstaller {
 			case "mcp":
 				await this.removeMcp(item, target)
 				break
+			// kilocode_change start - Handle skill type
+			case "skill":
+				// Skills are not removable through the marketplace - they are managed separately
+				throw new Error("Skills cannot be removed through the marketplace installer")
+			// kilocode_change end
 			default:
 				throw new Error(`Unsupported item type: ${(item as any).type}`)
 		}
 	}
 
-	private async removeMode(item: MarketplaceItem, target: "project" | "global"): Promise<void> {
+	// kilocode_change start - Use narrowed type for better type safety
+	private async removeMode(
+		item: Extract<MarketplaceItem, { type: "mode" }>,
+		target: "project" | "global",
+	): Promise<void> {
+		// kilocode_change end
 		if (!this.customModesManager) {
 			throw new Error("CustomModesManager is not available")
 		}
@@ -302,7 +321,7 @@ export class SimpleInstaller {
 		let content: string
 		if (Array.isArray(item.content)) {
 			// Array of McpInstallationMethod objects - use first method
-			content = item.content[0].content
+			content = (item.content as any)[0].content // kilocode_change - cast needed due to narrowed type
 		} else {
 			content = item.content || ""
 		}
@@ -328,7 +347,12 @@ export class SimpleInstaller {
 		await this.customModesManager.deleteCustomMode(modeSlug, true)
 	}
 
-	private async removeMcp(item: MarketplaceItem, target: "project" | "global"): Promise<void> {
+	// kilocode_change start - Use narrowed type for better type safety
+	private async removeMcp(
+		item: Extract<MarketplaceItem, { type: "mcp" }>,
+		target: "project" | "global",
+	): Promise<void> {
+		// kilocode_change end
 		const filePath = await this.getMcpFilePath(target)
 
 		try {
@@ -336,15 +360,6 @@ export class SimpleInstaller {
 			const existingData = JSON.parse(existing)
 
 			if (existingData?.mcpServers) {
-				// Parse the item content to get server names
-				let content: string
-				if (Array.isArray(item.content)) {
-					// Array of McpInstallationMethod objects - use first method
-					content = item.content[0].content
-				} else {
-					content = item.content
-				}
-
 				const serverName = item.id
 				delete existingData.mcpServers[serverName]
 

+ 320 - 0
src/services/marketplace/__tests__/RemoteConfigLoader.skill.spec.ts

@@ -0,0 +1,320 @@
+// kilocode_change - new file
+// npx vitest services/marketplace/__tests__/RemoteConfigLoader.skill.spec.ts
+//
+// This file contains tests specific to the skills marketplace functionality.
+// The main RemoteConfigLoader.spec.ts tests modes and MCPs, while this file
+// focuses on skill-specific behavior to minimize merge conflicts with upstream.
+
+import axios from "axios"
+import { RemoteConfigLoader } from "../RemoteConfigLoader"
+import type { MarketplaceItemType, SkillMarketplaceItem } from "@roo-code/types"
+
+// Mock axios
+vi.mock("axios")
+const mockedAxios = axios as any
+
+// Mock the cloud config
+vi.mock("@roo-code/cloud", () => ({
+	getRooCodeApiUrl: () => "https://test.api.com",
+}))
+
+vi.mock("@roo-code/types", async (importOriginal) => {
+	const actual = await importOriginal<typeof import("@roo-code/types")>()
+	return {
+		...actual,
+		getKiloBaseUriFromToken: () => "https://test.api.com",
+	}
+})
+
+describe("RemoteConfigLoader - Skills", () => {
+	let loader: RemoteConfigLoader
+
+	beforeEach(() => {
+		loader = new RemoteConfigLoader()
+		vi.clearAllMocks()
+		loader.clearCache()
+		process.env.KILOCODE_BACKEND_BASE_URL = "https://test.api.com"
+	})
+
+	afterEach(() => {
+		delete process.env.KILOCODE_BACKEND_BASE_URL
+	})
+
+	// Helper to create mock implementation
+	const createMockImplementation = (modesYaml: string, mcpsYaml: string, skillsYaml: string) => {
+		return (url: string) => {
+			if (url.includes("/modes")) {
+				return Promise.resolve({ data: modesYaml })
+			}
+			if (url.includes("/mcps")) {
+				return Promise.resolve({ data: mcpsYaml })
+			}
+			if (url.includes("/skills")) {
+				return Promise.resolve({ data: skillsYaml })
+			}
+			return Promise.reject(new Error("Unknown URL"))
+		}
+	}
+
+	describe("fetchSkills", () => {
+		it("should fetch and transform skills from API", async () => {
+			const mockModesYaml = `items: []`
+			const mockMcpsYaml = `items: []`
+			const mockSkillsYaml = `items:
+  - id: "test-skill"
+    description: "A test skill"
+    category: "testing"
+    githubUrl: "https://github.com/test/test-skill"
+    rawUrl: "https://raw.githubusercontent.com/test/test-skill/main/SKILL.md"`
+
+			mockedAxios.get.mockImplementation(createMockImplementation(mockModesYaml, mockMcpsYaml, mockSkillsYaml))
+
+			const items = await loader.loadAllItems()
+
+			expect(mockedAxios.get).toHaveBeenCalledWith(
+				"https://test.api.com/api/marketplace/skills",
+				expect.objectContaining({
+					timeout: 10000,
+					headers: {
+						Accept: "application/json",
+						"Content-Type": "application/json",
+					},
+				}),
+			)
+
+			const skills = items.filter((item) => item.type === "skill")
+			expect(skills).toHaveLength(1)
+			expect(skills[0]).toEqual({
+				type: "skill",
+				id: "test-skill",
+				name: "test-skill",
+				description: "A test skill",
+				category: "testing",
+				githubUrl: "https://github.com/test/test-skill",
+				rawUrl: "https://raw.githubusercontent.com/test/test-skill/main/SKILL.md",
+				displayName: "Test Skill",
+				displayCategory: "Testing",
+			})
+		})
+
+		it("should convert kebab-case id to Title Case displayName", async () => {
+			const mockModesYaml = `items: []`
+			const mockMcpsYaml = `items: []`
+			const mockSkillsYaml = `items:
+  - id: "my-awesome-skill"
+    description: "An awesome skill"
+    category: "code-generation"
+    githubUrl: "https://github.com/test/my-awesome-skill"
+    rawUrl: "https://raw.githubusercontent.com/test/my-awesome-skill/main/SKILL.md"`
+
+			mockedAxios.get.mockImplementation(createMockImplementation(mockModesYaml, mockMcpsYaml, mockSkillsYaml))
+
+			const items = await loader.loadAllItems()
+			const skill = items.find((item) => item.type === "skill") as SkillMarketplaceItem
+
+			expect(skill.displayName).toBe("My Awesome Skill")
+			expect(skill.displayCategory).toBe("Code Generation")
+		})
+
+		it("should handle single-word id and category", async () => {
+			const mockModesYaml = `items: []`
+			const mockMcpsYaml = `items: []`
+			const mockSkillsYaml = `items:
+  - id: "translation"
+    description: "A translation skill"
+    category: "localization"
+    githubUrl: "https://github.com/test/translation"
+    rawUrl: "https://raw.githubusercontent.com/test/translation/main/SKILL.md"`
+
+			mockedAxios.get.mockImplementation(createMockImplementation(mockModesYaml, mockMcpsYaml, mockSkillsYaml))
+
+			const items = await loader.loadAllItems()
+			const skill = items.find((item) => item.type === "skill") as SkillMarketplaceItem
+
+			expect(skill.displayName).toBe("Translation")
+			expect(skill.displayCategory).toBe("Localization")
+		})
+
+		it("should fetch multiple skills", async () => {
+			const mockModesYaml = `items: []`
+			const mockMcpsYaml = `items: []`
+			const mockSkillsYaml = `items:
+  - id: "skill-one"
+    description: "First skill"
+    category: "category-a"
+    githubUrl: "https://github.com/test/skill-one"
+    rawUrl: "https://raw.githubusercontent.com/test/skill-one/main/SKILL.md"
+  - id: "skill-two"
+    description: "Second skill"
+    category: "category-b"
+    githubUrl: "https://github.com/test/skill-two"
+    rawUrl: "https://raw.githubusercontent.com/test/skill-two/main/SKILL.md"
+  - id: "skill-three"
+    description: "Third skill"
+    category: "category-a"
+    githubUrl: "https://github.com/test/skill-three"
+    rawUrl: "https://raw.githubusercontent.com/test/skill-three/main/SKILL.md"`
+
+			mockedAxios.get.mockImplementation(createMockImplementation(mockModesYaml, mockMcpsYaml, mockSkillsYaml))
+
+			const items = await loader.loadAllItems()
+			const skills = items.filter((item) => item.type === "skill")
+
+			expect(skills).toHaveLength(3)
+			expect(skills.map((s) => s.id)).toEqual(["skill-one", "skill-two", "skill-three"])
+		})
+
+		it("should combine modes, MCPs, and skills in loadAllItems", async () => {
+			const mockModesYaml = `items:
+  - id: "test-mode"
+    name: "Test Mode"
+    description: "A test mode"
+    content: "test content"`
+
+			const mockMcpsYaml = `items:
+  - id: "test-mcp"
+    name: "Test MCP"
+    description: "A test MCP"
+    url: "https://github.com/test/test-mcp"
+    content: '{"command": "test"}'`
+
+			const mockSkillsYaml = `items:
+  - id: "test-skill"
+    description: "A test skill"
+    category: "testing"
+    githubUrl: "https://github.com/test/test-skill"
+    rawUrl: "https://raw.githubusercontent.com/test/test-skill/main/SKILL.md"`
+
+			mockedAxios.get.mockImplementation(createMockImplementation(mockModesYaml, mockMcpsYaml, mockSkillsYaml))
+
+			const items = await loader.loadAllItems()
+
+			expect(items).toHaveLength(3)
+			expect(items.filter((i) => i.type === "mode")).toHaveLength(1)
+			expect(items.filter((i) => i.type === "mcp")).toHaveLength(1)
+			expect(items.filter((i) => i.type === "skill")).toHaveLength(1)
+		})
+
+		it("should return empty array when no skills exist", async () => {
+			const mockModesYaml = `items: []`
+			const mockMcpsYaml = `items: []`
+			const mockSkillsYaml = `items: []`
+
+			mockedAxios.get.mockImplementation(createMockImplementation(mockModesYaml, mockMcpsYaml, mockSkillsYaml))
+
+			const items = await loader.loadAllItems()
+			const skills = items.filter((item) => item.type === "skill")
+
+			expect(skills).toHaveLength(0)
+		})
+
+		it("should cache skills separately", async () => {
+			const mockModesYaml = `items: []`
+			const mockMcpsYaml = `items: []`
+			const mockSkillsYaml = `items:
+  - id: "cached-skill"
+    description: "A cached skill"
+    category: "caching"
+    githubUrl: "https://github.com/test/cached-skill"
+    rawUrl: "https://raw.githubusercontent.com/test/cached-skill/main/SKILL.md"`
+
+			mockedAxios.get.mockImplementation(createMockImplementation(mockModesYaml, mockMcpsYaml, mockSkillsYaml))
+
+			// First call
+			const items1 = await loader.loadAllItems()
+			expect(mockedAxios.get).toHaveBeenCalledTimes(3)
+
+			// Second call - should use cache
+			const items2 = await loader.loadAllItems()
+			expect(mockedAxios.get).toHaveBeenCalledTimes(3) // Still 3, not 6
+
+			const skills1 = items1.filter((i) => i.type === "skill")
+			const skills2 = items2.filter((i) => i.type === "skill")
+			expect(skills1).toEqual(skills2)
+		})
+
+		it("should handle skills API failure gracefully", async () => {
+			const mockModesYaml = `items: []`
+			const mockMcpsYaml = `items: []`
+
+			mockedAxios.get.mockImplementation((url: string) => {
+				if (url.includes("/modes")) {
+					return Promise.resolve({ data: mockModesYaml })
+				}
+				if (url.includes("/mcps")) {
+					return Promise.resolve({ data: mockMcpsYaml })
+				}
+				if (url.includes("/skills")) {
+					return Promise.reject(new Error("Skills API unavailable"))
+				}
+				return Promise.reject(new Error("Unknown URL"))
+			})
+
+			// Should throw because skills fetch fails
+			await expect(loader.loadAllItems()).rejects.toThrow("Skills API unavailable")
+		})
+
+		it("should validate skill data schema", async () => {
+			const mockModesYaml = `items: []`
+			const mockMcpsYaml = `items: []`
+			// Missing required fields
+			const invalidSkillsYaml = `items:
+  - id: "invalid-skill"
+    # Missing description, category, githubUrl, rawUrl`
+
+			mockedAxios.get.mockImplementation(createMockImplementation(mockModesYaml, mockMcpsYaml, invalidSkillsYaml))
+
+			await expect(loader.loadAllItems()).rejects.toThrow()
+		})
+	})
+
+	describe("getItem with skills", () => {
+		it("should find skill by id and type", async () => {
+			const mockModesYaml = `items: []`
+			const mockMcpsYaml = `items: []`
+			const mockSkillsYaml = `items:
+  - id: "target-skill"
+    description: "The skill we want"
+    category: "targeting"
+    githubUrl: "https://github.com/test/target-skill"
+    rawUrl: "https://raw.githubusercontent.com/test/target-skill/main/SKILL.md"`
+
+			mockedAxios.get.mockImplementation(createMockImplementation(mockModesYaml, mockMcpsYaml, mockSkillsYaml))
+
+			const skillItem = await loader.getItem("target-skill", "skill" as MarketplaceItemType)
+			const notFound = await loader.getItem("nonexistent-skill", "skill" as MarketplaceItemType)
+
+			expect(skillItem).toEqual({
+				type: "skill",
+				id: "target-skill",
+				name: "target-skill",
+				description: "The skill we want",
+				category: "targeting",
+				githubUrl: "https://github.com/test/target-skill",
+				rawUrl: "https://raw.githubusercontent.com/test/target-skill/main/SKILL.md",
+				displayName: "Target Skill",
+				displayCategory: "Targeting",
+			})
+
+			expect(notFound).toBeNull()
+		})
+
+		it("should not return skill when searching for different type", async () => {
+			const mockModesYaml = `items: []`
+			const mockMcpsYaml = `items: []`
+			const mockSkillsYaml = `items:
+  - id: "my-skill"
+    description: "A skill"
+    category: "testing"
+    githubUrl: "https://github.com/test/my-skill"
+    rawUrl: "https://raw.githubusercontent.com/test/my-skill/main/SKILL.md"`
+
+			mockedAxios.get.mockImplementation(createMockImplementation(mockModesYaml, mockMcpsYaml, mockSkillsYaml))
+
+			// Search for skill id but with mode type
+			const result = await loader.getItem("my-skill", "mode" as MarketplaceItemType)
+
+			expect(result).toBeNull()
+		})
+	})
+})

+ 44 - 9
src/services/marketplace/__tests__/RemoteConfigLoader.spec.ts

@@ -60,12 +60,17 @@ describe("RemoteConfigLoader", () => {
 				if (url.includes("/mcps")) {
 					return Promise.resolve({ data: mockMcpsYaml })
 				}
+				// kilocode_change start
+				if (url.includes("/skills")) {
+					return Promise.resolve({ data: "items: []" })
+				}
+				// kilocode_change end
 				return Promise.reject(new Error("Unknown URL"))
 			})
 
 			const items = await loader.loadAllItems()
 
-			expect(mockedAxios.get).toHaveBeenCalledTimes(2)
+			expect(mockedAxios.get).toHaveBeenCalledTimes(3) // kilocode_change - now 3 calls (modes, mcps, skills)
 			expect(mockedAxios.get).toHaveBeenCalledWith(
 				"https://test.api.com/api/marketplace/modes",
 				expect.objectContaining({
@@ -126,16 +131,21 @@ describe("RemoteConfigLoader", () => {
 				if (url.includes("/mcps")) {
 					return Promise.resolve({ data: mockMcpsYaml })
 				}
+				// kilocode_change start
+				if (url.includes("/skills")) {
+					return Promise.resolve({ data: "items: []" })
+				}
+				// kilocode_change end
 				return Promise.reject(new Error("Unknown URL"))
 			})
 
 			// First call - should hit API
 			const items1 = await loader.loadAllItems()
-			expect(mockedAxios.get).toHaveBeenCalledTimes(2)
+			expect(mockedAxios.get).toHaveBeenCalledTimes(3) // kilocode_change - now 3 calls (modes, mcps, skills)
 
 			// Second call - should use cache
 			const items2 = await loader.loadAllItems()
-			expect(mockedAxios.get).toHaveBeenCalledTimes(2) // Still 2, not 4
+			expect(mockedAxios.get).toHaveBeenCalledTimes(3) // kilocode_change - still 3, not 6
 
 			expect(items1).toEqual(items2)
 		})
@@ -162,6 +172,11 @@ describe("RemoteConfigLoader", () => {
 				if (url.includes("/mcps")) {
 					return Promise.resolve({ data: mockMcpsYaml })
 				}
+				// kilocode_change start
+				if (url.includes("/skills")) {
+					return Promise.resolve({ data: "items: []" })
+				}
+				// kilocode_change end
 				return Promise.reject(new Error("Unknown URL"))
 			})
 
@@ -207,6 +222,11 @@ describe("RemoteConfigLoader", () => {
 				if (url.includes("/mcps")) {
 					return Promise.resolve({ data: validMcpsYaml })
 				}
+				// kilocode_change start
+				if (url.includes("/skills")) {
+					return Promise.resolve({ data: "items: []" })
+				}
+				// kilocode_change end
 				return Promise.reject(new Error("Unknown URL"))
 			})
 
@@ -237,6 +257,11 @@ describe("RemoteConfigLoader", () => {
 				if (url.includes("/mcps")) {
 					return Promise.resolve({ data: mockMcpsYaml })
 				}
+				// kilocode_change start
+				if (url.includes("/skills")) {
+					return Promise.resolve({ data: "items: []" })
+				}
+				// kilocode_change end
 				return Promise.reject(new Error("Unknown URL"))
 			})
 
@@ -282,23 +307,28 @@ describe("RemoteConfigLoader", () => {
 				if (url.includes("/mcps")) {
 					return Promise.resolve({ data: mockMcpsYaml })
 				}
+				// kilocode_change start
+				if (url.includes("/skills")) {
+					return Promise.resolve({ data: "items: []" })
+				}
+				// kilocode_change end
 				return Promise.reject(new Error("Unknown URL"))
 			})
 
 			// First call
 			await loader.loadAllItems()
-			expect(mockedAxios.get).toHaveBeenCalledTimes(2)
+			expect(mockedAxios.get).toHaveBeenCalledTimes(3) // kilocode_change - now 3 calls (modes, mcps, skills)
 
 			// Second call - should use cache
 			await loader.loadAllItems()
-			expect(mockedAxios.get).toHaveBeenCalledTimes(2)
+			expect(mockedAxios.get).toHaveBeenCalledTimes(3) // kilocode_change - still 3, not 6
 
 			// Clear cache
 			loader.clearCache()
 
 			// Third call - should hit API again
 			await loader.loadAllItems()
-			expect(mockedAxios.get).toHaveBeenCalledTimes(4)
+			expect(mockedAxios.get).toHaveBeenCalledTimes(6) // kilocode_change - 3 more calls
 		})
 	})
 
@@ -319,6 +349,11 @@ describe("RemoteConfigLoader", () => {
 				if (url.includes("/mcps")) {
 					return Promise.resolve({ data: mockMcpsYaml })
 				}
+				// kilocode_change start
+				if (url.includes("/skills")) {
+					return Promise.resolve({ data: "items: []" })
+				}
+				// kilocode_change end
 				return Promise.reject(new Error("Unknown URL"))
 			})
 
@@ -330,18 +365,18 @@ describe("RemoteConfigLoader", () => {
 
 			// First call
 			await loader.loadAllItems()
-			expect(mockedAxios.get).toHaveBeenCalledTimes(2)
+			expect(mockedAxios.get).toHaveBeenCalledTimes(3) // kilocode_change - now 3 calls (modes, mcps, skills)
 
 			// Second call immediately - should use cache
 			await loader.loadAllItems()
-			expect(mockedAxios.get).toHaveBeenCalledTimes(2)
+			expect(mockedAxios.get).toHaveBeenCalledTimes(3) // kilocode_change - still 3, not 6
 
 			// Advance time by 6 minutes (360,000 ms)
 			currentTime += 6 * 60 * 1000
 
 			// Third call - cache should be expired
 			await loader.loadAllItems()
-			expect(mockedAxios.get).toHaveBeenCalledTimes(4)
+			expect(mockedAxios.get).toHaveBeenCalledTimes(6) // kilocode_change - 3 more calls
 
 			// Restore original Date.now
 			Date.now = originalDateNow

+ 1 - 1
src/shared/WebviewMessage.ts

@@ -304,7 +304,7 @@ export interface WebviewMessage {
 	shareId?: string // kilocode_change - for sessionFork
 	sessionId?: string // kilocode_change - for sessionSelect
 	editedMessageContent?: string
-	tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud" | "auth" // kilocode_change
+	tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud" | "auth" | "skills" // kilocode_change
 	disabled?: boolean
 	context?: string
 	dataUri?: string

+ 36 - 7
webview-ui/src/components/marketplace/MarketplaceView.tsx

@@ -1,4 +1,5 @@
 import { useState, useEffect, useMemo, useContext } from "react"
+import { SKILLS_MARKETPLACE_ENABLED } from "@roo-code/types" // kilocode_change
 import { Button } from "@/components/ui/button"
 import { Tab, TabContent, TabHeader } from "../common/Tab"
 import { MarketplaceViewStateManager } from "./MarketplaceViewStateManager"
@@ -6,6 +7,7 @@ import { useStateManager } from "./useStateManager"
 import { useAppTranslation } from "@/i18n/TranslationContext"
 import { vscode } from "@/utils/vscode"
 import { MarketplaceListView } from "./MarketplaceListView"
+import { SkillsMarketplace } from "./SkillsMarketplace" // kilocode_change
 import { cn } from "@/lib/utils"
 import { TooltipProvider } from "@/components/ui/tooltip"
 import { ExtensionStateContext } from "@/context/ExtensionStateContext"
@@ -13,7 +15,7 @@ import { ExtensionStateContext } from "@/context/ExtensionStateContext"
 interface MarketplaceViewProps {
 	onDone?: () => void
 	stateManager: MarketplaceViewStateManager
-	targetTab?: "mcp" | "mode"
+	targetTab?: "mcp" | "mode" | "skills" // kilocode_change - Added skills tab
 	hideHeader?: boolean // kilocode_change
 }
 export function MarketplaceView({ stateManager, onDone, targetTab, hideHeader = false }: MarketplaceViewProps) {
@@ -44,7 +46,7 @@ export function MarketplaceView({ stateManager, onDone, targetTab, hideHeader =
 	}, [state.allItems, hasReceivedInitialState])
 
 	useEffect(() => {
-		if (targetTab && (targetTab === "mcp" || targetTab === "mode")) {
+		if (targetTab && (targetTab === "mcp" || targetTab === "mode" || targetTab === "skills")) {
 			manager.transition({ type: "SET_ACTIVE_TAB", payload: { tab: targetTab } })
 		}
 	}, [targetTab, manager])
@@ -117,6 +119,9 @@ export function MarketplaceView({ stateManager, onDone, targetTab, hideHeader =
 	)
 	// kilocode_change end - Header messages for each tab
 
+	// kilocode_change - Skills are now fetched as part of fetchMarketplaceData
+	// No separate fetch needed - skills come with the unified marketplace data
+
 	return (
 		<TooltipProvider delayDuration={300}>
 			{/* kilocode_change: header conditionally className relative or fixed */}
@@ -141,15 +146,21 @@ export function MarketplaceView({ stateManager, onDone, targetTab, hideHeader =
 					<div className="w-full mt-2">
 						<div className="flex relative py-1">
 							<div className="absolute w-full h-[2px] -bottom-[2px] bg-vscode-input-border">
+								{/* kilocode_change start - Tab indicator with fixed 33.33% width */}
 								<div
 									className={cn(
-										"absolute w-1/2 h-[2px] bottom-0 bg-vscode-button-background transition-all duration-300 ease-in-out",
-										{
-											"left-0": state.activeTab === "mcp",
-											"left-1/2": state.activeTab === "mode",
-										},
+										"absolute h-[2px] bottom-0 bg-vscode-button-background transition-all duration-300 ease-in-out w-1/3",
 									)}
+									style={{
+										left:
+											state.activeTab === "mcp"
+												? "0%"
+												: state.activeTab === "mode"
+													? "33.33%"
+													: "66.66%",
+									}}
 								/>
+								{/* kilocode_change end */}
 							</div>
 							<button
 								className="flex items-center justify-center gap-2 flex-1 text-sm font-medium rounded-sm transition-colors duration-300 relative z-10 text-vscode-foreground"
@@ -163,6 +174,19 @@ export function MarketplaceView({ stateManager, onDone, targetTab, hideHeader =
 								}>
 								Modes
 							</button>
+							{/* kilocode_change start - Skills tab button or filler div */}
+							{SKILLS_MARKETPLACE_ENABLED ? (
+								<button
+									className="flex items-center justify-center gap-2 flex-1 text-sm font-medium rounded-sm transition-colors duration-300 relative z-10 text-vscode-foreground"
+									onClick={() =>
+										manager.transition({ type: "SET_ACTIVE_TAB", payload: { tab: "skills" } })
+									}>
+									Skills
+								</button>
+							) : (
+								<div className="flex-1" />
+							)}
+							{/* kilocode_change end */}
 						</div>
 					</div>
 				</TabHeader>
@@ -186,6 +210,11 @@ export function MarketplaceView({ stateManager, onDone, targetTab, hideHeader =
 							headerMessage={modesHeaderMessage} // kilocode_change
 						/>
 					)}
+					{/* kilocode_change start - Skills marketplace tab content */}
+					{SKILLS_MARKETPLACE_ENABLED && state.activeTab === "skills" && (
+						<SkillsMarketplace skills={stateManager.getSkills()} isLoading={state.isFetching} />
+					)}
+					{/* kilocode_change end */}
 				</TabContent>
 			</Tab>
 		</TooltipProvider>

+ 9 - 3
webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts

@@ -11,10 +11,10 @@
  * 3. Using minimal state updates to avoid resetting scroll position
  */
 
-import { MarketplaceItem } from "@roo-code/types"
+import { MarketplaceItem, isSkillItem, SkillMarketplaceItem } from "@roo-code/types" //kilo_code change
 import { vscode } from "../../utils/vscode"
 import { WebviewMessage } from "../../../../src/shared/WebviewMessage"
-import type { MarketplaceInstalledMetadata } from "../../../../src/shared/ExtensionMessage"
+import type { MarketplaceInstalledMetadata } from "../../../../src/shared/ExtensionMessage" //kilo_code change
 
 export interface ViewState {
 	allItems: MarketplaceItem[]
@@ -22,7 +22,7 @@ export interface ViewState {
 	displayItems?: MarketplaceItem[] // Items currently being displayed (filtered or all)
 	displayOrganizationMcps?: MarketplaceItem[] // Organization MCPs currently being displayed (filtered or all)
 	isFetching: boolean
-	activeTab: "mcp" | "mode"
+	activeTab: "mcp" | "mode" | "skills" // kilocode_change added skills
 	filters: {
 		type: string
 		search: string
@@ -483,4 +483,10 @@ export class MarketplaceViewStateManager {
 			this.notifyStateChange()
 		}
 	}
+
+	// kilocode_change start
+	public getSkills(): SkillMarketplaceItem[] {
+		return this.state.allItems.filter(isSkillItem)
+	}
+	// kilocode_change end
 }

+ 115 - 0
webview-ui/src/components/marketplace/SkillsMarketplace.tsx

@@ -0,0 +1,115 @@
+// kilocode_change new file
+
+import React, { useMemo, useState } from "react"
+import { SkillMarketplaceItem } from "@roo-code/types"
+import { useAppTranslation } from "@/i18n/TranslationContext"
+import { SkillItemCard } from "./components/SkillItemCard"
+import { Input } from "@/components/ui/input"
+import { Button } from "@/components/ui/button"
+import { cn } from "@/lib/utils"
+
+interface SkillsMarketplaceProps {
+	skills: SkillMarketplaceItem[]
+	isLoading: boolean
+}
+
+export const SkillsMarketplace: React.FC<SkillsMarketplaceProps> = ({ skills, isLoading }) => {
+	const { t } = useAppTranslation()
+	const [searchQuery, setSearchQuery] = useState("")
+	const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
+
+	// Get unique categories from skills with their display names
+	const categoryMap = useMemo(() => {
+		const map = new Map<string, string>()
+		skills.forEach((skill) => {
+			if (!map.has(skill.category)) {
+				map.set(skill.category, skill.displayCategory)
+			}
+		})
+		return map
+	}, [skills])
+
+	// Get sorted category keys
+	const categories = useMemo(() => {
+		return Array.from(categoryMap.keys()).sort()
+	}, [categoryMap])
+
+	// Filter skills based on search query and selected category
+	const filteredSkills = useMemo(() => {
+		return skills.filter((skill) => {
+			const matchesSearch =
+				searchQuery === "" ||
+				skill.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
+				skill.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
+				skill.category.toLowerCase().includes(searchQuery.toLowerCase())
+
+			const matchesCategory = selectedCategory === null || skill.category === selectedCategory
+
+			return matchesSearch && matchesCategory
+		})
+	}, [skills, searchQuery, selectedCategory])
+
+	if (isLoading) {
+		return (
+			<div className="flex items-center justify-center p-8">
+				<div className="text-vscode-descriptionForeground">{t("marketplace:skills.loading")}</div>
+			</div>
+		)
+	}
+
+	return (
+		<div className="flex flex-col gap-4">
+			{/* Search and filter controls */}
+			<div className="flex flex-col gap-2">
+				<Input
+					type="text"
+					placeholder={t("marketplace:skills.searchPlaceholder")}
+					value={searchQuery}
+					onChange={(e) => setSearchQuery(e.target.value)}
+					className="w-full"
+				/>
+
+				{/* Category filter buttons */}
+				{categories.length > 0 && (
+					<div className="flex flex-wrap gap-1">
+						<Button
+							size="sm"
+							variant={selectedCategory === null ? "primary" : "secondary"}
+							className={cn("text-xs h-6 py-0 px-2")}
+							onClick={() => setSelectedCategory(null)}>
+							{t("marketplace:skills.allCategories")}
+						</Button>
+						{categories.map((category) => (
+							<Button
+								key={category}
+								size="sm"
+								variant={selectedCategory === category ? "primary" : "secondary"}
+								className={cn("text-xs h-6 py-0 px-2")}
+								onClick={() => setSelectedCategory(selectedCategory === category ? null : category)}>
+								{categoryMap.get(category)}
+							</Button>
+						))}
+					</div>
+				)}
+			</div>
+
+			{/* Results count */}
+			<div className="text-sm text-vscode-descriptionForeground">
+				{t("marketplace:skills.resultsCount", { count: filteredSkills.length })}
+			</div>
+
+			{/* Skills list */}
+			{filteredSkills.length === 0 ? (
+				<div className="flex items-center justify-center p-8">
+					<div className="text-vscode-descriptionForeground">{t("marketplace:skills.noResults")}</div>
+				</div>
+			) : (
+				<div className="flex flex-col gap-2">
+					{filteredSkills.map((skill) => (
+						<SkillItemCard key={skill.id} skill={skill} />
+					))}
+				</div>
+			)}
+		</div>
+	)
+}

+ 12 - 5
webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx

@@ -1,5 +1,5 @@
 import React, { useState, useMemo, useEffect } from "react"
-import { MarketplaceItem, McpParameter, McpInstallationMethod } from "@roo-code/types"
+import { MarketplaceItem, McpParameter, McpInstallationMethod, isSkillItem } from "@roo-code/types"
 import { vscode } from "@/utils/vscode"
 import { useAppTranslation } from "@/i18n/TranslationContext"
 import {
@@ -46,12 +46,12 @@ export const MarketplaceInstallModal: React.FC<MarketplaceInstallModalProps> = (
 
 	// Check if item has multiple installation methods
 	const hasMultipleMethods = useMemo(() => {
-		return item && Array.isArray(item.content) && item.content.length > 1
+		return item && !isSkillItem(item) && Array.isArray(item.content) && item.content.length > 1 // kilocode_change: skills don't have content
 	}, [item])
 
 	// Get installation method names (for display in dropdown)
 	const methodNames = useMemo(() => {
-		if (!item || !Array.isArray(item.content)) return []
+		if (!item || isSkillItem(item) || !Array.isArray(item.content)) return [] // kilocode_change: skills don't have content
 
 		// Content is an array of McpInstallationMethod objects
 		return (item.content as Array<{ name: string; content: string }>).map((method) => method.name)
@@ -59,7 +59,7 @@ export const MarketplaceInstallModal: React.FC<MarketplaceInstallModalProps> = (
 
 	// Get effective parameters for the selected method (global + method-specific)
 	const effectiveParameters = useMemo(() => {
-		if (!item) return []
+		if (!item || isSkillItem(item)) return [] // kilocode_change: skills don't have content/parameters
 
 		const globalParams = item.type === "mcp" ? item.parameters || [] : []
 		let methodParams: McpParameter[] = []
@@ -86,7 +86,7 @@ export const MarketplaceInstallModal: React.FC<MarketplaceInstallModalProps> = (
 		let methodPrereqs: string[] = []
 
 		// Get method-specific prerequisites if content is an array
-		if (Array.isArray(item.content)) {
+		if (!isSkillItem(item) && Array.isArray(item.content) /* kilocode_change skills don't have content*/) {
 			const selectedMethod = item.content[selectedMethodIndex] as McpInstallationMethod
 			methodPrereqs = selectedMethod?.prerequisites || []
 		}
@@ -99,6 +99,13 @@ export const MarketplaceInstallModal: React.FC<MarketplaceInstallModalProps> = (
 	// Update parameter values when method changes
 	React.useEffect(() => {
 		if (item) {
+			// kilocode_change start: skills don't have content/parameters
+			if (isSkillItem(item)) {
+				setParameterValues({})
+				return
+			}
+			// kilocode_change end
+
 			// Get effective parameters for current method
 			const globalParams = item.type === "mcp" ? item.parameters || [] : []
 			let methodParams: McpParameter[] = []

+ 61 - 0
webview-ui/src/components/marketplace/components/SkillItemCard.tsx

@@ -0,0 +1,61 @@
+// kilocode_change new file
+
+import React from "react"
+import { SkillMarketplaceItem } from "@roo-code/types"
+import { vscode } from "@/utils/vscode"
+import { useAppTranslation } from "@/i18n/TranslationContext"
+import { Button } from "@/components/ui/button"
+
+interface SkillItemCardProps {
+	skill: SkillMarketplaceItem
+}
+
+export const SkillItemCard: React.FC<SkillItemCardProps> = ({ skill }) => {
+	const { t } = useAppTranslation()
+
+	const handleViewOnGitHub = () => {
+		vscode.postMessage({ type: "openExternal", url: skill.githubUrl })
+	}
+
+	const { displayName, displayCategory } = skill
+
+	return (
+		<div className="border border-vscode-panel-border rounded-sm p-3 bg-vscode-editor-background">
+			<div className="flex gap-2 items-start justify-between">
+				<div className="flex gap-2 items-start">
+					<div>
+						<h3 className="text-lg font-semibold text-vscode-foreground mt-0 mb-1 leading-none">
+							<Button
+								variant="link"
+								className="p-0 h-auto text-lg font-semibold text-vscode-foreground hover:underline"
+								onClick={handleViewOnGitHub}>
+								{displayName}
+							</Button>
+						</h3>
+						<p className="text-sm text-vscode-descriptionForeground my-0">
+							{t("marketplace:skills.category", { category: displayCategory })}
+						</p>
+					</div>
+				</div>
+				<div className="flex items-center gap-1">
+					<Button
+						size="sm"
+						variant="secondary"
+						className="text-xs h-5 py-0 px-2"
+						onClick={handleViewOnGitHub}>
+						{t("marketplace:skills.viewOnGitHub")}
+					</Button>
+				</div>
+			</div>
+
+			<p className="my-2 text-vscode-foreground">{skill.description}</p>
+
+			{/* Category badge */}
+			<div className="relative flex flex-wrap gap-1 my-2">
+				<span className="text-xs px-2 py-0.5 rounded-sm h-5 flex items-center bg-vscode-badge-background text-vscode-badge-foreground">
+					{displayCategory}
+				</span>
+			</div>
+		</div>
+	)
+}

+ 3 - 0
webview-ui/src/i18n/locales/ar/kilocode.json

@@ -306,6 +306,9 @@
 		},
 		"mcp": {
 			"description": "يتم صيانة خوادم MCP هذه من قبل المجتمع. انقر <1>هنا</1> لتعديل إعدادات MCP الخاصة بك."
+		},
+		"skills": {
+			"description": "تصفح واكتشف المهارات من المجتمع. توفر المهارات تعليمات متخصصة لمهام محددة."
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/ar/marketplace.json

@@ -152,5 +152,13 @@
 	},
 	"footer": {
 		"issueText": "واجهت مشكلة أو عندك اقتراح لسوق العناصر؟ <0>افتح تذكرة على GitHub</0> وعطنا خبر!"
+	},
+	"skills": {
+		"loading": "جاري تحميل المهارات...",
+		"searchPlaceholder": "ابحث عن المهارات...",
+		"allCategories": "الكل",
+		"resultsCount": "{{count}} مهارة تم العثور عليها",
+		"noResults": "لم يتم العثور على مهارات تطابق معاييرك",
+		"viewOnGitHub": "عرض على GitHub"
 	}
 }

+ 3 - 0
webview-ui/src/i18n/locales/ca/kilocode.json

@@ -308,6 +308,9 @@
 		},
 		"mcp": {
 			"description": "Aquests servidors MCP són mantinguts per la comunitat. Fes clic <1>aquí</1> per editar els teus propis ajustos de MCP."
+		},
+		"skills": {
+			"description": "Explora i descobreix habilitats de la comunitat. Les habilitats proporcionen instruccions especialitzades per a tasques específiques."
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/ca/marketplace.json

@@ -154,5 +154,13 @@
 	},
 	"footer": {
 		"issueText": "Has trobat un problema amb un element del marketplace o tens suggeriments per a nous elements? <0>Obre una incidència de GitHub</0> per fer-nos-ho saber!"
+	},
+	"skills": {
+		"loading": "Carregant habilitats...",
+		"searchPlaceholder": "Cercar habilitats...",
+		"allCategories": "Totes",
+		"resultsCount": "{{count}} habilitats trobades",
+		"noResults": "No s'han trobat habilitats que coincideixin amb els teus criteris",
+		"viewOnGitHub": "Veure a GitHub"
 	}
 }

+ 3 - 0
webview-ui/src/i18n/locales/cs/kilocode.json

@@ -307,6 +307,9 @@
 		},
 		"modes": {
 			"description": "Tyto režimy jsou dostupné od komunity. Klikněte <1>zde</1> pro úpravu vašich vlastních režimů."
+		},
+		"skills": {
+			"description": "Procházejte a objevujte dovednosti od komunity. Dovednosti poskytují specializované instrukce pro konkrétní úkoly."
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/cs/marketplace.json

@@ -152,5 +152,13 @@
 	},
 	"footer": {
 		"issueText": "Našel jsi problém s položkou marketplace nebo máš návrhy na nové? <0>Otevři GitHub issue</0> a dej nám vědět!"
+	},
+	"skills": {
+		"loading": "Načítání dovedností...",
+		"searchPlaceholder": "Hledat dovednosti...",
+		"allCategories": "Vše",
+		"resultsCount": "Nalezeno {{count}} dovedností",
+		"noResults": "Nenalezeny žádné dovednosti odpovídající tvým kritériím",
+		"viewOnGitHub": "Zobrazit na GitHubu"
 	}
 }

+ 3 - 0
webview-ui/src/i18n/locales/de/kilocode.json

@@ -307,6 +307,9 @@
 		},
 		"modes": {
 			"description": "Diese Modi werden von der Community bereitgestellt. Klicken Sie <1>hier</1>, um Ihre eigenen benutzerdefinierten Modi zu bearbeiten."
+		},
+		"skills": {
+			"description": "Durchstöbere und entdecke Skills aus der Community. Skills bieten spezialisierte Anweisungen für bestimmte Aufgaben."
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/de/marketplace.json

@@ -154,5 +154,13 @@
 	},
 	"footer": {
 		"issueText": "Problem mit einem Marketplace-Element gefunden oder Vorschläge für neue? <0>Öffne ein GitHub-Issue</0>, um es uns mitzuteilen!"
+	},
+	"skills": {
+		"loading": "Skills werden geladen...",
+		"searchPlaceholder": "Skills durchsuchen...",
+		"allCategories": "Alle",
+		"resultsCount": "{{count}} Skills gefunden",
+		"noResults": "Keine Skills gefunden, die deinen Kriterien entsprechen",
+		"viewOnGitHub": "Auf GitHub anzeigen"
 	}
 }

+ 3 - 0
webview-ui/src/i18n/locales/en/kilocode.json

@@ -306,6 +306,9 @@
 		},
 		"modes": {
 			"description": "These modes are available from the community. Click <1>here</1> to edit your own custom modes."
+		},
+		"skills": {
+			"description": "Browse and discover skills from the community. Skills provide specialized instructions for specific tasks."
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/en/marketplace.json

@@ -152,5 +152,13 @@
 	},
 	"footer": {
 		"issueText": "Found a problem with a marketplace item or have suggestions for new ones? <0>Open a GitHub issue</0> to let us know!"
+	},
+	"skills": {
+		"loading": "Loading skills...",
+		"searchPlaceholder": "Search skills...",
+		"allCategories": "All",
+		"resultsCount": "{{count}} skills found",
+		"noResults": "No skills found matching your criteria",
+		"viewOnGitHub": "View on GitHub"
 	}
 }

+ 3 - 0
webview-ui/src/i18n/locales/es/kilocode.json

@@ -307,6 +307,9 @@
 		},
 		"modes": {
 			"description": "Estos modos están disponibles en la comunidad. Haz clic <1>aquí</1> para editar tus propios modos personalizados."
+		},
+		"skills": {
+			"description": "Explora y descubre habilidades de la comunidad. Las habilidades proporcionan instrucciones especializadas para tareas específicas."
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/es/marketplace.json

@@ -154,5 +154,13 @@
 	},
 	"footer": {
 		"issueText": "¿Encontraste un problema con un elemento del marketplace o tienes sugerencias para nuevos? ¡<0>Abre un issue en GitHub</0> para hacérnoslo saber!"
+	},
+	"skills": {
+		"loading": "Cargando habilidades...",
+		"searchPlaceholder": "Buscar habilidades...",
+		"allCategories": "Todas",
+		"resultsCount": "{{count}} habilidades encontradas",
+		"noResults": "No se encontraron habilidades que coincidan con tus criterios",
+		"viewOnGitHub": "Ver en GitHub"
 	}
 }

+ 3 - 0
webview-ui/src/i18n/locales/fr/kilocode.json

@@ -307,6 +307,9 @@
 		},
 		"modes": {
 			"description": "Ces modes sont disponibles auprès de la communauté. Cliquez <1>ici</1> pour modifier vos propres modes personnalisés."
+		},
+		"skills": {
+			"description": "Parcourez et découvrez des compétences de la communauté. Les compétences fournissent des instructions spécialisées pour des tâches spécifiques."
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/fr/marketplace.json

@@ -154,5 +154,13 @@
 	},
 	"footer": {
 		"issueText": "Vous avez trouvé un problème avec un élément du marketplace ou avez des suggestions pour de nouveaux éléments ? <0>Ouvrez une issue GitHub</0> pour nous le faire savoir !"
+	},
+	"skills": {
+		"loading": "Chargement des compétences...",
+		"searchPlaceholder": "Rechercher des compétences...",
+		"allCategories": "Toutes",
+		"resultsCount": "{{count}} compétences trouvées",
+		"noResults": "Aucune compétence ne correspond à tes critères",
+		"viewOnGitHub": "Voir sur GitHub"
 	}
 }

+ 3 - 0
webview-ui/src/i18n/locales/hi/kilocode.json

@@ -307,6 +307,9 @@
 		},
 		"mcp": {
 			"description": "ये MCP सर्वर समुदाय द्वारा बनाए रखे जाते हैं। अपनी खुद की MCP सेटिंग्स को संपादित करने के लिए <1>यहां</1> क्लिक करें।"
+		},
+		"skills": {
+			"description": "समुदाय से कौशल ब्राउज़ करें और खोजें। कौशल विशिष्ट कार्यों के लिए विशेष निर्देश प्रदान करते हैं।"
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/hi/marketplace.json

@@ -152,5 +152,13 @@
 	},
 	"footer": {
 		"issueText": "कोई marketplace आइटम के साथ समस्या है या नए आइटम के लिए सुझाव हैं? <0>GitHub issue खोलें</0> हमें बताने के लिए!"
+	},
+	"skills": {
+		"loading": "स्किल्स लोड हो रहे हैं...",
+		"searchPlaceholder": "स्किल्स खोजें...",
+		"allCategories": "सभी",
+		"resultsCount": "{{count}} स्किल्स मिले",
+		"noResults": "आपके मापदंडों से मेल खाने वाले कोई स्किल्स नहीं मिले",
+		"viewOnGitHub": "GitHub पर देखें"
 	}
 }

+ 3 - 0
webview-ui/src/i18n/locales/id/kilocode.json

@@ -307,6 +307,9 @@
 		},
 		"modes": {
 			"description": "Mode-mode ini tersedia dari komunitas. Klik <1>di sini</1> untuk mengedit mode kustom Anda sendiri."
+		},
+		"skills": {
+			"description": "Jelajahi dan temukan skill dari komunitas. Skill menyediakan instruksi khusus untuk tugas-tugas tertentu."
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/id/marketplace.json

@@ -152,5 +152,13 @@
 	},
 	"footer": {
 		"issueText": "Menemukan masalah dengan item marketplace atau punya saran untuk yang baru? <0>Buka GitHub issue</0> untuk memberi tahu kami!"
+	},
+	"skills": {
+		"loading": "Memuat skill...",
+		"searchPlaceholder": "Cari skill...",
+		"allCategories": "Semua",
+		"resultsCount": "{{count}} skill ditemukan",
+		"noResults": "Tidak ada skill yang cocok dengan kriteria Anda",
+		"viewOnGitHub": "Lihat di GitHub"
 	}
 }

+ 3 - 0
webview-ui/src/i18n/locales/it/kilocode.json

@@ -308,6 +308,9 @@
 		},
 		"modes": {
 			"description": "Queste modalità sono disponibili dalla community. Clicca <1>qui</1> per modificare le tue modalità personalizzate."
+		},
+		"skills": {
+			"description": "Esplora e scopri skill dalla community. Le skill forniscono istruzioni specializzate per compiti specifici."
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/it/marketplace.json

@@ -152,5 +152,13 @@
 	},
 	"footer": {
 		"issueText": "Hai trovato un problema con un elemento del marketplace o hai suggerimenti per nuovi elementi? <0>Apri un issue GitHub</0> per farcelo sapere!"
+	},
+	"skills": {
+		"loading": "Caricamento skill...",
+		"searchPlaceholder": "Cerca skill...",
+		"allCategories": "Tutte",
+		"resultsCount": "{{count}} skill trovate",
+		"noResults": "Nessuna skill trovata corrispondente ai tuoi criteri",
+		"viewOnGitHub": "Visualizza su GitHub"
 	}
 }

+ 3 - 0
webview-ui/src/i18n/locales/ja/kilocode.json

@@ -308,6 +308,9 @@
 		},
 		"mcp": {
 			"description": "これらのMCPサーバーはコミュニティによって管理されています。<1>こちら</1>をクリックして、あなた自身のMCP設定を編集してください。"
+		},
+		"skills": {
+			"description": "コミュニティのスキルを閲覧・発見できます。スキルは特定のタスクに対する専門的な指示を提供します。"
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/ja/marketplace.json

@@ -152,5 +152,13 @@
 	},
 	"footer": {
 		"issueText": "Marketplaceアイテムで問題を見つけた、または新しいアイテムの提案がありますか?<0>GitHub issueを開いて</0>お知らせください!"
+	},
+	"skills": {
+		"loading": "スキルを読み込み中...",
+		"searchPlaceholder": "スキルを検索...",
+		"allCategories": "すべて",
+		"resultsCount": "{{count}}個のスキルが見つかりました",
+		"noResults": "条件に一致するスキルが見つかりません",
+		"viewOnGitHub": "GitHubで表示"
 	}
 }

+ 3 - 0
webview-ui/src/i18n/locales/ko/kilocode.json

@@ -307,6 +307,9 @@
 		},
 		"modes": {
 			"description": "이러한 모드는 커뮤니티에서 제공됩니다. <1>여기</1>를 클릭하여 자신만의 사용자 정의 모드를 편집하세요."
+		},
+		"skills": {
+			"description": "커뮤니티의 스킬을 탐색하고 발견하세요. 스킬은 특정 작업에 대한 전문 지침을 제공합니다."
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/ko/marketplace.json

@@ -152,5 +152,13 @@
 	},
 	"footer": {
 		"issueText": "Marketplace 아이템에 문제가 있거나 새로운 아이템에 대한 제안이 있나요? <0>GitHub issue를 열어서</0> 알려주세요!"
+	},
+	"skills": {
+		"loading": "스킬 로딩 중...",
+		"searchPlaceholder": "스킬 검색...",
+		"allCategories": "전체",
+		"resultsCount": "{{count}}개 스킬 발견",
+		"noResults": "조건에 맞는 스킬을 찾을 수 없습니다",
+		"viewOnGitHub": "GitHub에서 보기"
 	}
 }

+ 3 - 0
webview-ui/src/i18n/locales/nl/kilocode.json

@@ -306,6 +306,9 @@
 		},
 		"modes": {
 			"description": "Deze modi zijn beschikbaar via de gemeenschap. Klik <1>hier</1> om je eigen aangepaste modi te bewerken."
+		},
+		"skills": {
+			"description": "Blader door en ontdek skills van de gemeenschap. Skills bieden gespecialiseerde instructies voor specifieke taken."
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/nl/marketplace.json

@@ -152,5 +152,13 @@
 	},
 	"footer": {
 		"issueText": "Heb je een probleem gevonden met een marketplace-item of heb je suggesties voor nieuwe items? <0>Open een GitHub issue</0> om het ons te laten weten!"
+	},
+	"skills": {
+		"loading": "Skills laden...",
+		"searchPlaceholder": "Skills zoeken...",
+		"allCategories": "Alle",
+		"resultsCount": "{{count}} skills gevonden",
+		"noResults": "Geen skills gevonden die aan je criteria voldoen",
+		"viewOnGitHub": "Bekijken op GitHub"
 	}
 }

+ 3 - 0
webview-ui/src/i18n/locales/pl/kilocode.json

@@ -308,6 +308,9 @@
 		},
 		"modes": {
 			"description": "Te tryby są dostępne ze społeczności. Kliknij <1>tutaj</1>, aby edytować swoje własne niestandardowe tryby."
+		},
+		"skills": {
+			"description": "Przeglądaj i odkrywaj umiejętności od społeczności. Umiejętności zapewniają specjalistyczne instrukcje dla konkretnych zadań."
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/pl/marketplace.json

@@ -152,5 +152,13 @@
 	},
 	"footer": {
 		"issueText": "Znalazłeś problem z elementem marketplace lub masz sugestie dotyczące nowych elementów? <0>Otwórz issue na GitHub</0>, aby nam o tym powiedzieć!"
+	},
+	"skills": {
+		"loading": "Ładowanie umiejętności...",
+		"searchPlaceholder": "Szukaj umiejętności...",
+		"allCategories": "Wszystkie",
+		"resultsCount": "Znaleziono {{count}} umiejętności",
+		"noResults": "Nie znaleziono umiejętności pasujących do twoich kryteriów",
+		"viewOnGitHub": "Zobacz na GitHubie"
 	}
 }

+ 3 - 0
webview-ui/src/i18n/locales/pt-BR/kilocode.json

@@ -307,6 +307,9 @@
 		},
 		"modes": {
 			"description": "Estes modos estão disponíveis na comunidade. Clique <1>aqui</1> para editar seus próprios modos personalizados."
+		},
+		"skills": {
+			"description": "Navegue e descubra habilidades da comunidade. As habilidades fornecem instruções especializadas para tarefas específicas."
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/pt-BR/marketplace.json

@@ -152,5 +152,13 @@
 	},
 	"footer": {
 		"issueText": "Encontrou um problema com um item do marketplace ou tem sugestões para novos itens? <0>Abra um issue no GitHub</0> para nos avisar!"
+	},
+	"skills": {
+		"loading": "Carregando habilidades...",
+		"searchPlaceholder": "Buscar habilidades...",
+		"allCategories": "Todas",
+		"resultsCount": "{{count}} habilidades encontradas",
+		"noResults": "Nenhuma habilidade encontrada que corresponda aos seus critérios",
+		"viewOnGitHub": "Ver no GitHub"
 	}
 }

+ 3 - 0
webview-ui/src/i18n/locales/ru/kilocode.json

@@ -308,6 +308,9 @@
 		},
 		"modes": {
 			"description": "Эти режимы доступны от сообщества. Нажмите <1>здесь</1>, чтобы редактировать свои собственные пользовательские режимы."
+		},
+		"skills": {
+			"description": "Просматривайте и открывайте навыки от сообщества. Навыки предоставляют специализированные инструкции для конкретных задач."
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/ru/marketplace.json

@@ -152,5 +152,13 @@
 	},
 	"footer": {
 		"issueText": "Нашли проблему с элементом marketplace или есть предложения для новых элементов? <0>Откройте issue на GitHub</0>, чтобы сообщить нам!"
+	},
+	"skills": {
+		"loading": "Загрузка навыков...",
+		"searchPlaceholder": "Поиск навыков...",
+		"allCategories": "Все",
+		"resultsCount": "Найдено {{count}} навыков",
+		"noResults": "Навыки, соответствующие вашим критериям, не найдены",
+		"viewOnGitHub": "Посмотреть на GitHub"
 	}
 }

+ 3 - 0
webview-ui/src/i18n/locales/th/kilocode.json

@@ -307,6 +307,9 @@
 		},
 		"mcp": {
 			"description": "เซิร์ฟเวอร์ MCP เหล่านี้ได้รับการดูแลโดยชุมชน คลิก <1>ที่นี่</1> เพื่อแก้ไขการตั้งค่า MCP ของคุณ"
+		},
+		"skills": {
+			"description": "เรียกดูและค้นพบทักษะจากชุมชน ทักษะให้คำแนะนำเฉพาะทางสำหรับงานเฉพาะ"
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/th/marketplace.json

@@ -152,5 +152,13 @@
 	},
 	"footer": {
 		"issueText": "พบปัญหากับรายการ marketplace หรือมีข้อเสนอแนะสำหรับรายการใหม่? <0>เปิด GitHub issue</0> เพื่อแจ้งให้เราทราบ!"
+	},
+	"skills": {
+		"loading": "กำลังโหลดทักษะ...",
+		"searchPlaceholder": "ค้นหาทักษะ...",
+		"allCategories": "ทั้งหมด",
+		"resultsCount": "พบ {{count}} ทักษะ",
+		"noResults": "ไม่พบทักษะที่ตรงกับเกณฑ์ของคุณ",
+		"viewOnGitHub": "ดูบน GitHub"
 	}
 }

+ 3 - 0
webview-ui/src/i18n/locales/tr/kilocode.json

@@ -307,6 +307,9 @@
 		},
 		"modes": {
 			"description": "Bu modlar topluluk tarafından sağlanmaktadır. Kendi özel modlarınızı düzenlemek için <1>buraya</1> tıklayın."
+		},
+		"skills": {
+			"description": "Topluluktan becerilere göz atın ve keşfedin. Beceriler, belirli görevler için özel talimatlar sağlar."
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/tr/marketplace.json

@@ -152,5 +152,13 @@
 	},
 	"footer": {
 		"issueText": "Bir marketplace öğesi ile ilgili sorun bulduğun veya yeni öğeler için önerilerin var mı? <0>GitHub'da issue aç</0> ve bize bildir!"
+	},
+	"skills": {
+		"loading": "Beceriler yükleniyor...",
+		"searchPlaceholder": "Beceri ara...",
+		"allCategories": "Tümü",
+		"resultsCount": "{{count}} beceri bulundu",
+		"noResults": "Kriterlerinize uyan beceri bulunamadı",
+		"viewOnGitHub": "GitHub'da görüntüle"
 	}
 }

+ 3 - 0
webview-ui/src/i18n/locales/uk/kilocode.json

@@ -307,6 +307,9 @@
 		},
 		"modes": {
 			"description": "Ці режими доступні від спільноти. Натисніть <1>тут</1>, щоб редагувати власні користувацькі режими."
+		},
+		"skills": {
+			"description": "Переглядайте та відкривайте навички від спільноти. Навички надають спеціалізовані інструкції для конкретних завдань."
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/uk/marketplace.json

@@ -152,5 +152,13 @@
 	},
 	"footer": {
 		"issueText": "Знайшов проблему з елементом marketplace або маєш пропозиції щодо нових? <0>Відкрий GitHub issue</0>, щоб повідомити нам!"
+	},
+	"skills": {
+		"loading": "Завантаження навичок...",
+		"searchPlaceholder": "Шукати навички...",
+		"allCategories": "Усі",
+		"resultsCount": "Знайдено {{count}} навичок",
+		"noResults": "Не знайдено навичок, що відповідають твоїм критеріям",
+		"viewOnGitHub": "Переглянути на GitHub"
 	}
 }

+ 3 - 0
webview-ui/src/i18n/locales/vi/kilocode.json

@@ -308,6 +308,9 @@
 		},
 		"modes": {
 			"description": "Những chế độ này được cung cấp từ cộng đồng. Nhấp <1>vào đây</1> để chỉnh sửa chế độ tùy chỉnh của riêng bạn."
+		},
+		"skills": {
+			"description": "Duyệt và khám phá các kỹ năng từ cộng đồng. Kỹ năng cung cấp hướng dẫn chuyên biệt cho các tác vụ cụ thể."
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/vi/marketplace.json

@@ -152,5 +152,13 @@
 	},
 	"footer": {
 		"issueText": "Bạn tìm thấy vấn đề với mục marketplace hoặc có đề xuất cho mục mới? <0>Mở issue GitHub</0> để cho chúng tôi biết!"
+	},
+	"skills": {
+		"loading": "Đang tải kỹ năng...",
+		"searchPlaceholder": "Tìm kiếm kỹ năng...",
+		"allCategories": "Tất cả",
+		"resultsCount": "Tìm thấy {{count}} kỹ năng",
+		"noResults": "Không tìm thấy kỹ năng nào phù hợp với tiêu chí của bạn",
+		"viewOnGitHub": "Xem trên GitHub"
 	}
 }

+ 3 - 0
webview-ui/src/i18n/locales/zh-CN/kilocode.json

@@ -308,6 +308,9 @@
 		},
 		"modes": {
 			"description": "这些模式来自社区。点击<1>此处</1>编辑您自己的自定义模式。"
+		},
+		"skills": {
+			"description": "浏览和发现来自社区的技能。技能为特定任务提供专业指导。"
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/zh-CN/marketplace.json

@@ -152,5 +152,13 @@
 	},
 	"footer": {
 		"issueText": "发现 marketplace 项目问题或有新项目建议?<0>在 GitHub 开启 issue</0> 告诉我们!"
+	},
+	"skills": {
+		"loading": "正在加载技能...",
+		"searchPlaceholder": "搜索技能...",
+		"allCategories": "全部",
+		"resultsCount": "找到 {{count}} 个技能",
+		"noResults": "未找到符合条件的技能",
+		"viewOnGitHub": "在 GitHub 上查看"
 	}
 }

+ 3 - 0
webview-ui/src/i18n/locales/zh-TW/kilocode.json

@@ -307,6 +307,9 @@
 		},
 		"modes": {
 			"description": "这些模式来自社区提供。点击<1>这里</1>编辑您自己的自定义模式。"
+		},
+		"skills": {
+			"description": "瀏覽並探索來自社群的技能。技能為特定任務提供專業指導。"
 		}
 	},
 	"modes": {

+ 8 - 0
webview-ui/src/i18n/locales/zh-TW/marketplace.json

@@ -152,5 +152,13 @@
 	},
 	"footer": {
 		"issueText": "發現市集項目有問題或有新建議嗎?<0>到 GitHub 開個 issue</0> 告訴我們!"
+	},
+	"skills": {
+		"loading": "正在載入技能...",
+		"searchPlaceholder": "搜尋技能...",
+		"allCategories": "全部",
+		"resultsCount": "找到 {{count}} 個技能",
+		"noResults": "找不到符合條件的技能",
+		"viewOnGitHub": "在 GitHub 上檢視"
 	}
 }