From 84053147613d5593cab8ef56fb00e95c7b9bfff7 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: Sat, 4 Jul 2026 22:08:16 +0800 Subject: [PATCH] feat: refine skill market cards and drawer --- desktop/src/i18n/locales/en.ts | 13 ++ desktop/src/i18n/locales/jp.ts | 13 ++ desktop/src/i18n/locales/kr.ts | 13 ++ desktop/src/i18n/locales/zh-TW.ts | 13 ++ desktop/src/i18n/locales/zh.ts | 13 ++ desktop/src/pages/SkillCenter.test.tsx | 89 +++++++- desktop/src/pages/SkillCenter.tsx | 303 ++++++++++++++++--------- 7 files changed, 350 insertions(+), 107 deletions(-) diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts index a8e77fe7..e072495b 100644 --- a/desktop/src/i18n/locales/en.ts +++ b/desktop/src/i18n/locales/en.ts @@ -2027,6 +2027,9 @@ export const en = { 'skillCenter.marketplace.filter.apiKey': 'API key', 'skillCenter.marketplace.total': 'Total', 'skillCenter.marketplace.loadedCount': '{count} loaded', + 'skillCenter.marketplace.status.loaded': '{count} loaded', + 'skillCenter.marketplace.status.safe': '{safe}/{total} verified', + 'skillCenter.marketplace.status.installed': '{count} installed', 'skillCenter.marketplace.totalHint': 'Current result set', 'skillCenter.marketplace.safe': 'Verified', 'skillCenter.marketplace.safeHint': 'Clean or trusted signals', @@ -2053,6 +2056,10 @@ export const en = { 'skillCenter.marketplace.sort.updated': 'Updated', 'skillCenter.marketplace.sort.trending': 'Trending', 'skillCenter.marketplace.installedBadge': 'Installed', + 'skillCenter.marketplace.previewAvailable': 'Preview', + 'skillCenter.marketplace.noPreviewBadge': 'No preview', + 'skillCenter.marketplace.cardMeta.downloads': '{count} downloads', + 'skillCenter.marketplace.cardMeta.stars': '{count} stars', 'skillCenter.marketplace.trust.clean': 'Clean', 'skillCenter.marketplace.trust.benign': 'Benign', 'skillCenter.marketplace.trust.signed': 'Signed', @@ -2074,6 +2081,12 @@ export const en = { 'skillCenter.marketplace.risk.requires-api-key': 'API key', 'skillCenter.marketplace.entryPreview': 'Entry preview', 'skillCenter.marketplace.filePreview': 'File preview', + 'skillCenter.marketplace.drawer.summary': 'Summary', + 'skillCenter.marketplace.drawer.decision': 'Install decision', + 'skillCenter.marketplace.drawer.safety': 'Safety', + 'skillCenter.marketplace.drawer.metadata': 'Metadata', + 'skillCenter.marketplace.drawer.links': 'Links', + 'skillCenter.marketplace.loadingCard': 'Loading marketplace skills', 'skillCenter.marketplace.previewTruncated': 'Truncated', 'skillCenter.marketplace.previewUnavailable': 'Preview unavailable', 'skillCenter.marketplace.previewUnavailable.skillhub': 'SkillHub does not expose a safe raw file preview endpoint yet.', diff --git a/desktop/src/i18n/locales/jp.ts b/desktop/src/i18n/locales/jp.ts index 5a1ed0a6..8796da8a 100644 --- a/desktop/src/i18n/locales/jp.ts +++ b/desktop/src/i18n/locales/jp.ts @@ -2029,6 +2029,9 @@ export const jp: Record = { 'skillCenter.marketplace.filter.apiKey': 'API キー', 'skillCenter.marketplace.total': '合計', 'skillCenter.marketplace.loadedCount': '{count} 件読み込み済み', + 'skillCenter.marketplace.status.loaded': '{count} 件読み込み済み', + 'skillCenter.marketplace.status.safe': '{safe}/{total} 検証済み', + 'skillCenter.marketplace.status.installed': '{count} 件インストール済み', 'skillCenter.marketplace.totalHint': '現在の結果', 'skillCenter.marketplace.safe': '信頼済み', 'skillCenter.marketplace.safeHint': '安全または信頼シグナル', @@ -2055,6 +2058,10 @@ export const jp: Record = { 'skillCenter.marketplace.sort.updated': '更新日時', 'skillCenter.marketplace.sort.trending': '人気', 'skillCenter.marketplace.installedBadge': 'インストール済み', + 'skillCenter.marketplace.previewAvailable': 'プレビュー', + 'skillCenter.marketplace.noPreviewBadge': 'プレビューなし', + 'skillCenter.marketplace.cardMeta.downloads': '{count} ダウンロード', + 'skillCenter.marketplace.cardMeta.stars': '{count} スター', 'skillCenter.marketplace.trust.clean': '安全', 'skillCenter.marketplace.trust.benign': '低リスク', 'skillCenter.marketplace.trust.signed': '署名済み', @@ -2076,6 +2083,12 @@ export const jp: Record = { 'skillCenter.marketplace.risk.requires-api-key': 'API キー', 'skillCenter.marketplace.entryPreview': 'エントリプレビュー', 'skillCenter.marketplace.filePreview': 'ファイルプレビュー', + 'skillCenter.marketplace.drawer.summary': '概要', + 'skillCenter.marketplace.drawer.decision': 'インストール判断', + 'skillCenter.marketplace.drawer.safety': '安全', + 'skillCenter.marketplace.drawer.metadata': 'メタデータ', + 'skillCenter.marketplace.drawer.links': 'リンク', + 'skillCenter.marketplace.loadingCard': 'マーケットスキルを読み込み中', 'skillCenter.marketplace.previewTruncated': '切り詰め済み', 'skillCenter.marketplace.previewUnavailable': 'プレビュー不可', 'skillCenter.marketplace.previewUnavailable.skillhub': 'SkillHub はまだ安全な raw ファイルプレビュー API を提供していません。', diff --git a/desktop/src/i18n/locales/kr.ts b/desktop/src/i18n/locales/kr.ts index 64984e71..24abe59d 100644 --- a/desktop/src/i18n/locales/kr.ts +++ b/desktop/src/i18n/locales/kr.ts @@ -2029,6 +2029,9 @@ export const kr: Record = { 'skillCenter.marketplace.filter.apiKey': 'API 키', 'skillCenter.marketplace.total': '전체', 'skillCenter.marketplace.loadedCount': '{count}개 로드됨', + 'skillCenter.marketplace.status.loaded': '{count}개 로드됨', + 'skillCenter.marketplace.status.safe': '{safe}/{total} 검증됨', + 'skillCenter.marketplace.status.installed': '{count}개 설치됨', 'skillCenter.marketplace.totalHint': '현재 결과', 'skillCenter.marketplace.safe': '신뢰됨', 'skillCenter.marketplace.safeHint': '안전 또는 신뢰 신호', @@ -2055,6 +2058,10 @@ export const kr: Record = { 'skillCenter.marketplace.sort.updated': '업데이트', 'skillCenter.marketplace.sort.trending': '인기', 'skillCenter.marketplace.installedBadge': '설치됨', + 'skillCenter.marketplace.previewAvailable': '미리보기', + 'skillCenter.marketplace.noPreviewBadge': '미리보기 없음', + 'skillCenter.marketplace.cardMeta.downloads': '{count} 다운로드', + 'skillCenter.marketplace.cardMeta.stars': '{count} 스타', 'skillCenter.marketplace.trust.clean': '안전', 'skillCenter.marketplace.trust.benign': '낮은 위험', 'skillCenter.marketplace.trust.signed': '서명됨', @@ -2076,6 +2083,12 @@ export const kr: Record = { 'skillCenter.marketplace.risk.requires-api-key': 'API 키', 'skillCenter.marketplace.entryPreview': '엔트리 미리보기', 'skillCenter.marketplace.filePreview': '파일 미리보기', + 'skillCenter.marketplace.drawer.summary': '요약', + 'skillCenter.marketplace.drawer.decision': '설치 판단', + 'skillCenter.marketplace.drawer.safety': '안전', + 'skillCenter.marketplace.drawer.metadata': '메타데이터', + 'skillCenter.marketplace.drawer.links': '링크', + 'skillCenter.marketplace.loadingCard': '마켓 스킬 로드 중', 'skillCenter.marketplace.previewTruncated': '잘림', 'skillCenter.marketplace.previewUnavailable': '미리보기 불가', 'skillCenter.marketplace.previewUnavailable.skillhub': 'SkillHub는 아직 안전한 원본 파일 미리보기 API를 제공하지 않습니다.', diff --git a/desktop/src/i18n/locales/zh-TW.ts b/desktop/src/i18n/locales/zh-TW.ts index bdd6a001..b914cb84 100644 --- a/desktop/src/i18n/locales/zh-TW.ts +++ b/desktop/src/i18n/locales/zh-TW.ts @@ -2029,6 +2029,9 @@ export const zh: Record = { 'skillCenter.marketplace.filter.apiKey': '需 Key', 'skillCenter.marketplace.total': '總數', 'skillCenter.marketplace.loadedCount': '已載入 {count} 個', + 'skillCenter.marketplace.status.loaded': '已載入 {count} 個', + 'skillCenter.marketplace.status.safe': '{safe}/{total} 已驗證', + 'skillCenter.marketplace.status.installed': '已安裝 {count} 個', 'skillCenter.marketplace.totalHint': '目前結果集', 'skillCenter.marketplace.safe': '可信', 'skillCenter.marketplace.safeHint': '安全或可信訊號', @@ -2055,6 +2058,10 @@ export const zh: Record = { 'skillCenter.marketplace.sort.updated': '最近更新', 'skillCenter.marketplace.sort.trending': '熱門', 'skillCenter.marketplace.installedBadge': '已安裝', + 'skillCenter.marketplace.previewAvailable': '可預覽', + 'skillCenter.marketplace.noPreviewBadge': '無預覽', + 'skillCenter.marketplace.cardMeta.downloads': '{count} 次下載', + 'skillCenter.marketplace.cardMeta.stars': '{count} 星標', 'skillCenter.marketplace.trust.clean': '安全', 'skillCenter.marketplace.trust.benign': '低風險', 'skillCenter.marketplace.trust.signed': '已簽名', @@ -2076,6 +2083,12 @@ export const zh: Record = { 'skillCenter.marketplace.risk.requires-api-key': 'API Key', 'skillCenter.marketplace.entryPreview': '入口預覽', 'skillCenter.marketplace.filePreview': '檔案預覽', + 'skillCenter.marketplace.drawer.summary': '摘要', + 'skillCenter.marketplace.drawer.decision': '安裝決策', + 'skillCenter.marketplace.drawer.safety': '安全', + 'skillCenter.marketplace.drawer.metadata': '中繼資料', + 'skillCenter.marketplace.drawer.links': '連結', + 'skillCenter.marketplace.loadingCard': '正在載入市場技能', 'skillCenter.marketplace.previewTruncated': '已截斷', 'skillCenter.marketplace.previewUnavailable': '暫時無法預覽', 'skillCenter.marketplace.previewUnavailable.skillhub': 'SkillHub 目前尚未提供安全的原始檔案預覽介面。', diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index eae6ab84..a2f34005 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -2029,6 +2029,9 @@ export const zh: Record = { 'skillCenter.marketplace.filter.apiKey': '需 Key', 'skillCenter.marketplace.total': '总数', 'skillCenter.marketplace.loadedCount': '已加载 {count} 个', + 'skillCenter.marketplace.status.loaded': '已加载 {count} 个', + 'skillCenter.marketplace.status.safe': '{safe}/{total} 已验证', + 'skillCenter.marketplace.status.installed': '已安装 {count} 个', 'skillCenter.marketplace.totalHint': '当前结果集', 'skillCenter.marketplace.safe': '可信', 'skillCenter.marketplace.safeHint': '安全或可信信号', @@ -2055,6 +2058,10 @@ export const zh: Record = { 'skillCenter.marketplace.sort.updated': '最近更新', 'skillCenter.marketplace.sort.trending': '热门', 'skillCenter.marketplace.installedBadge': '已安装', + 'skillCenter.marketplace.previewAvailable': '可预览', + 'skillCenter.marketplace.noPreviewBadge': '无预览', + 'skillCenter.marketplace.cardMeta.downloads': '{count} 次下载', + 'skillCenter.marketplace.cardMeta.stars': '{count} 星标', 'skillCenter.marketplace.trust.clean': '安全', 'skillCenter.marketplace.trust.benign': '低风险', 'skillCenter.marketplace.trust.signed': '已签名', @@ -2076,6 +2083,12 @@ export const zh: Record = { 'skillCenter.marketplace.risk.requires-api-key': 'API Key', 'skillCenter.marketplace.entryPreview': '入口预览', 'skillCenter.marketplace.filePreview': '文件预览', + 'skillCenter.marketplace.drawer.summary': '摘要', + 'skillCenter.marketplace.drawer.decision': '安装决策', + 'skillCenter.marketplace.drawer.safety': '安全', + 'skillCenter.marketplace.drawer.metadata': '元数据', + 'skillCenter.marketplace.drawer.links': '链接', + 'skillCenter.marketplace.loadingCard': '正在加载市场技能', 'skillCenter.marketplace.previewTruncated': '已截断', 'skillCenter.marketplace.previewUnavailable': '暂时无法预览', 'skillCenter.marketplace.previewUnavailable.skillhub': 'SkillHub 目前还没有提供安全的原始文件预览接口。', diff --git a/desktop/src/pages/SkillCenter.test.tsx b/desktop/src/pages/SkillCenter.test.tsx index c659eb64..9c8469c8 100644 --- a/desktop/src/pages/SkillCenter.test.tsx +++ b/desktop/src/pages/SkillCenter.test.tsx @@ -1,4 +1,4 @@ -import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react' +import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react' import '@testing-library/jest-dom' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { skillMarketApi } from '../api/skillMarket' @@ -185,6 +185,93 @@ describe('SkillCenter', () => { }) }) + it('renders marketplace cards with trust, preview, install, API key, and popularity signals', async () => { + mockedSkillMarketApi.list.mockResolvedValue({ + items: [ + makeItem({ + slug: 'skill-vetter', + displayName: 'Skill Vetter', + summary: 'Security-first skill vetting.', + downloads: 260960, + trustState: 'clean', + installed: false, + }), + makeItem({ + slug: 'weather', + displayName: 'Weather', + summary: 'Fetch current weather through a command-line workflow.', + downloads: 18420, + trustState: 'clean', + installed: true, + requiresApiKey: false, + }), + makeItem({ + slug: 'crm-sync', + displayName: 'CRM Sync', + summary: 'Connects to an external CRM API.', + downloads: 7420, + trustState: 'warning', + requiresApiKey: true, + }), + ], + nextCursor: null, + source: 'clawhub', + sourceStatus: 'ok', + }) + + render() + + const market = screen.getByTestId('skill-marketplace-tab') + const vetterCard = await within(market).findByRole('button', { name: 'Skill Vetter' }) + const weatherCard = within(market).getByRole('button', { name: 'Weather' }) + const crmCard = within(market).getByRole('button', { name: 'CRM Sync' }) + expect(vetterCard).toBeInTheDocument() + expect(within(market).getAllByText('Clean').length).toBeGreaterThan(0) + expect(within(market).getAllByText('Preview').length).toBeGreaterThan(0) + expect(within(weatherCard).getByText('Installed')).toBeInTheDocument() + expect(within(crmCard).getByText('API key')).toBeInTheDocument() + expect(within(vetterCard).getByText(/260\.9K|260,960/)).toBeInTheDocument() + }) + + it('uses marketplace-shaped loading skeletons while the catalog loads', () => { + mockedSkillMarketApi.list.mockReturnValue(new Promise(() => {})) + act(() => { + useSkillMarketStore.setState({ + isLoading: true, + items: [], + }) + }) + + render() + + expect(screen.getByTestId('skill-marketplace-loading')).toBeInTheDocument() + expect(screen.getAllByTestId('skill-card-skeleton')).toHaveLength(8) + }) + + it('places install decision, blocking reason, and file preview in the drawer decision area', async () => { + mockedSkillMarketApi.detail.mockResolvedValue({ + detail: makeDetail({ + installEligibility: { status: 'blocked', reason: 'A full package safety scan is required before install.' }, + trustSummary: 'Scanner found no known malicious content, but the package scan is incomplete.', + filePreviews: [{ + path: 'SKILL.md', + content: '# Skill Vetter\n\nSecurity-first skill vetting.', + language: 'markdown', + size: 512, + }], + }), + }) + + render() + fireEvent.click(await screen.findByRole('button', { name: 'PPT Generator' })) + + const dialog = await screen.findByRole('dialog', { name: 'PPT Generator' }) + expect(within(dialog).getByRole('button', { name: 'Blocked' })).toBeDisabled() + expect(within(dialog).getByText(/full package safety scan/i)).toBeInTheDocument() + expect(within(dialog).getByText('File preview')).toBeInTheDocument() + expect(within(dialog).getByText('SKILL.md')).toBeInTheDocument() + }) + it('renders raw Markdown and Python previews for an uninstalled marketplace skill', async () => { mockedSkillMarketApi.detail.mockResolvedValue({ detail: makeDetail({ diff --git a/desktop/src/pages/SkillCenter.tsx b/desktop/src/pages/SkillCenter.tsx index aa457866..eaaf3c9b 100644 --- a/desktop/src/pages/SkillCenter.tsx +++ b/desktop/src/pages/SkillCenter.tsx @@ -105,10 +105,6 @@ export function SkillCenter() { () => items.filter((item) => TRUST_SAFE.includes(item.trustState)).length, [items], ) - const totalDownloads = useMemo( - () => items.reduce((sum, item) => sum + (item.downloads ?? item.installs ?? 0), 0), - [items], - ) const filteredItems = useMemo( () => items.filter((item) => matchesMarketFilter(item, marketFilter)), [items, marketFilter], @@ -279,7 +275,6 @@ export function SkillCenter() { total={items.length} safeCount={safeCount} installedCount={installedCount} - totalDownloads={totalDownloads} loading={isLoading} /> @@ -298,7 +293,7 @@ export function SkillCenter() {
{isLoading ? ( - + ) : items.length === 0 ? (
, @@ -884,7 +929,7 @@ function FilePreviewSection({ detail }: { detail: SkillMarketDetail }) { return null } return ( -
+