From b6aeaf01096a224ebd6070e69abba27f08e50c99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Fri, 3 Jul 2026 23:31:34 +0800 Subject: [PATCH] feat: add skill marketplace preview flow Tested: bun test src/server/__tests__/skill-market.test.ts Tested: cd desktop && bun run test -- SkillCenter.test.tsx skillMarketStore.test.ts generalSettings.test.ts ComputerUseSettings.test.ts --run Tested: cd desktop && bun run test -- persistenceMigrations.test.ts --run Tested: bun run check:desktop Tested: real browser smoke for Skill Vetter across white, light, and dark themes Not-tested: live install of an uninstalled marketplace skill to disk Confidence: medium Scope-risk: moderate --- desktop/src/__tests__/agentsSettings.test.tsx | 105 -- .../src/components/controls/ModelSelector.tsx | 2 +- desktop/src/components/layout/Sidebar.tsx | 2 +- .../src/components/shared/DirectoryPicker.tsx | 2 +- desktop/src/i18n/locales/en.ts | 48 + desktop/src/i18n/locales/jp.ts | 48 + desktop/src/i18n/locales/kr.ts | 48 + desktop/src/i18n/locales/zh-TW.ts | 48 + desktop/src/i18n/locales/zh.ts | 48 + desktop/src/lib/persistenceMigrations.test.ts | 32 + desktop/src/lib/persistenceMigrations.ts | 12 +- desktop/src/pages/ComputerUseSettings.tsx | 6 +- desktop/src/pages/ScheduledTasksEmpty.tsx | 2 +- desktop/src/pages/ScheduledTasksList.tsx | 2 +- desktop/src/pages/Settings.tsx | 8 +- desktop/src/pages/SkillCenter.test.tsx | 146 ++- desktop/src/pages/SkillCenter.tsx | 1081 +++++++++++++---- desktop/src/stores/skillMarketStore.test.ts | 62 + desktop/src/stores/skillMarketStore.ts | 71 +- desktop/src/theme/globals.css | 14 +- desktop/src/types/skillMarket.ts | 11 + src/server/__tests__/fixtures/skill-market.ts | 68 ++ src/server/__tests__/skill-market.test.ts | 176 ++- src/server/api/skill-market.ts | 54 +- .../services/skillMarket/clawhubAdapter.ts | 152 ++- src/server/services/skillMarket/service.ts | 276 ++++- src/server/services/skillMarket/types.ts | 11 + 27 files changed, 2110 insertions(+), 425 deletions(-) diff --git a/desktop/src/__tests__/agentsSettings.test.tsx b/desktop/src/__tests__/agentsSettings.test.tsx index 4831d974..633ec9b9 100644 --- a/desktop/src/__tests__/agentsSettings.test.tsx +++ b/desktop/src/__tests__/agentsSettings.test.tsx @@ -4,7 +4,6 @@ import '@testing-library/jest-dom' import { Settings } from '../pages/Settings' import { useAgentStore } from '../stores/agentStore' -import { useSkillStore } from '../stores/skillStore' import { useSettingsStore } from '../stores/settingsStore' import { useSessionStore } from '../stores/sessionStore' import { SETTINGS_TAB_ID, useTabStore } from '../stores/tabStore' @@ -41,10 +40,6 @@ vi.mock('../pages/AdapterSettings', () => ({ AdapterSettings: () =>
Adapter Settings Mock
, })) -vi.mock('../components/chat/CodeViewer', () => ({ - CodeViewer: ({ code }: { code: string }) =>
{code}
, -})) - const MOCK_AGENTS = [ { agentType: 'code-reviewer', @@ -97,50 +92,10 @@ const MOCK_AGENTS = [ }, ] -const MOCK_SKILL_DETAIL = { - meta: { - name: 'skill-docs', - displayName: 'Skill Docs', - description: 'A rich skill readme', - source: 'user' as const, - userInvocable: true, - contentLength: 200, - hasDirectory: true, - }, - tree: [ - { name: 'SKILL.md', path: 'SKILL.md', type: 'file' as const }, - { name: 'helper.ts', path: 'helper.ts', type: 'file' as const }, - ], - files: [ - { - path: 'SKILL.md', - language: 'markdown', - content: '# Heading\n\nParagraph with `inline code`.\n\n## Section\n\n- First item\n- Second item\n\n> Helpful quote', - body: '# Heading\n\nParagraph with `inline code`.\n\n## Section\n\n- First item\n- Second item\n\n> Helpful quote', - isEntry: true, - frontmatter: { - description: 'A rich skill readme', - model: 'sonnet', - }, - }, - { - path: 'helper.ts', - language: 'typescript', - content: 'export const helper = true', - isEntry: false, - }, - ], - skillRoot: '/tmp/skill-docs', -} - function switchToAgentsTab() { fireEvent.click(screen.getByText('Agents')) } -function switchToSkillsTab() { - fireEvent.click(screen.getByText('Skills')) -} - describe('Settings > Agents tab', () => { beforeEach(() => { useSettingsStore.setState({ locale: 'en' }) @@ -182,16 +137,6 @@ describe('Settings > Agents tab', () => { fetchAgents: noopFetch, selectAgent: (agent) => useAgentStore.setState({ selectedAgent: agent }), }) - useSkillStore.setState({ - skills: [], - selectedSkill: null, - isLoading: false, - isDetailLoading: false, - error: null, - fetchSkills: noopFetch, - fetchSkillDetail: noopFetch, - clearSelection: () => useSkillStore.setState({ selectedSkill: null }), - }) }) it('renders the Agents tab button in sidebar', () => { @@ -340,53 +285,3 @@ describe('Settings > Agents tab', () => { expect(screen.getByText('Installed Plugins')).toBeInTheDocument() }) }) - -describe('Settings > Skills tab', () => { - beforeEach(() => { - useSettingsStore.setState({ locale: 'en' }) - useSkillStore.setState({ - skills: [], - selectedSkill: null, - isLoading: false, - isDetailLoading: false, - error: null, - fetchSkills: noopFetch, - fetchSkillDetail: noopFetch, - clearSelection: () => useSkillStore.setState({ selectedSkill: null }), - }) - }) - - it('renders markdown skills with document styling in detail view', () => { - useSkillStore.setState({ - selectedSkill: MOCK_SKILL_DETAIL, - clearSelection: () => useSkillStore.setState({ selectedSkill: null }), - }) - - render() - switchToSkillsTab() - - expect(screen.getByText('Skill metadata')).toBeInTheDocument() - expect(screen.getByRole('heading', { name: 'Heading' })).toBeInTheDocument() - - const rendererRoot = screen.getByRole('heading', { name: 'Heading' }).closest('div[class*="prose"]') - expect(rendererRoot?.className).toContain('max-w-[72ch]') - expect(rendererRoot?.className).toContain('prose-h2:border-b') - expect(rendererRoot?.className).toContain('prose-p:text-[15px]') - expect(screen.getByText('Helpful quote')).toBeInTheDocument() - }) - - it('keeps code files rendered in CodeViewer instead of markdown prose', () => { - useSkillStore.setState({ - selectedSkill: MOCK_SKILL_DETAIL, - clearSelection: () => useSkillStore.setState({ selectedSkill: null }), - }) - - render() - switchToSkillsTab() - - fireEvent.click(screen.getAllByText('helper.ts')[0]!) - - expect(screen.getByTestId('code-viewer')).toHaveTextContent('export const helper = true') - expect(screen.queryByRole('heading', { name: 'Heading' })).not.toBeInTheDocument() - }) -}) diff --git a/desktop/src/components/controls/ModelSelector.tsx b/desktop/src/components/controls/ModelSelector.tsx index ef5dd659..2b505dc4 100644 --- a/desktop/src/components/controls/ModelSelector.tsx +++ b/desktop/src/components/controls/ModelSelector.tsx @@ -504,7 +504,7 @@ export const ModelSelector = forwardRef(function Mod className={` rounded-lg py-2 text-center text-xs font-semibold transition-colors ${isSelected - ? 'bg-[var(--color-brand)] text-white' + ? 'bg-[var(--color-brand)] text-[var(--color-on-primary)]' : 'bg-[var(--color-surface-container-high)] text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)]' } `} diff --git a/desktop/src/components/layout/Sidebar.tsx b/desktop/src/components/layout/Sidebar.tsx index 98d3f936..1754ac3c 100644 --- a/desktop/src/components/layout/Sidebar.tsx +++ b/desktop/src/components/layout/Sidebar.tsx @@ -998,7 +998,7 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {