From 77d912f49497a3d08cc63c76deaa0f9ac4e6ef8b Mon Sep 17 00:00:00 2001 From: hqhq1025 <1506751656@qq.com> Date: Sat, 22 Aug 2026 23:57:07 +0800 Subject: [PATCH] fix(skills): show installed catalog state --- .../renderer/styles/module-pages/skills.css | 5 ++ .../skills-panel-install-state.test.tsx | 60 +++++++++++++++++++ packages/ui/src/skills-copy.ts | 5 +- packages/ui/src/skills-panel.tsx | 14 ++++- 4 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 packages/ui/src/__tests__/skills-panel-install-state.test.tsx diff --git a/apps/desktop/src/renderer/styles/module-pages/skills.css b/apps/desktop/src/renderer/styles/module-pages/skills.css index 604ccd58a9..c5ace23891 100644 --- a/apps/desktop/src/renderer/styles/module-pages/skills.css +++ b/apps/desktop/src/renderer/styles/module-pages/skills.css @@ -36,6 +36,11 @@ color: var(--muted-foreground); } +.maka-skill-install-complete-icon { + display: inline-flex; + color: var(--success); +} + .maka-skill-inspector { min-width: 0; } diff --git a/packages/ui/src/__tests__/skills-panel-install-state.test.tsx b/packages/ui/src/__tests__/skills-panel-install-state.test.tsx new file mode 100644 index 0000000000..80e3a9ceba --- /dev/null +++ b/packages/ui/src/__tests__/skills-panel-install-state.test.tsx @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { LocaleProvider } from '../locale-context.js'; +import { SkillsModuleMain } from '../skills-panel.js'; +import { ToastProvider } from '../toast.js'; + +function renderBundledSkill(installed: boolean): string { + return renderToStaticMarkup( + + + undefined} + /> + + , + ); +} + +test('an available bundled skill renders the install action', () => { + const markup = renderBundledSkill(false); + assert.match(markup, /aria-label="Install Computer Use"/); + assert.match(markup, /lucide-download/); + assert.doesNotMatch(markup, /maka-skill-install-complete-icon/); +}); + +test('an installed bundled skill replaces the download action with a completion check', () => { + const markup = renderBundledSkill(true); + assert.match(markup, /aria-label="Computer Use is installed in this workspace"/); + assert.match(markup, /maka-skill-install-complete-icon/); + assert.match(markup, /lucide-check/); + assert.doesNotMatch(markup, /lucide-download/); +}); diff --git a/packages/ui/src/skills-copy.ts b/packages/ui/src/skills-copy.ts index f48a163ed8..5f2f569563 100644 --- a/packages/ui/src/skills-copy.ts +++ b/packages/ui/src/skills-copy.ts @@ -45,6 +45,7 @@ export interface SkillsCopy { tabs: { ariaLabel: string; market: string; builtin: string; installed: string }; install: { action: (name: string) => string; + installedAction: (name: string) => string; installedTitle: string; installed: string; notInstalled: string; @@ -162,7 +163,7 @@ const SKILLS_COPY = { categories: { '内容创作': '内容创作', '数据与AI': '数据与 AI', '设计与UI': '设计与 UI', 'DevOps与部署': 'DevOps 与部署', '文档与写作': '文档与写作', '效率工具': '效率工具', '研究与分析': '研究与分析' }, market: { categoryAll: '全部分类', sortName: '排序:名称', sortRecent: '排序:最近', controls: '市场筛选与排序', categoryFilter: '按分类筛选市场技能', sortAriaLabel: '市场技能排序方式', ariaLabel: '技能市场', importLocal: '导入本地 Skill', emptySearchTitle: '没有匹配的市场技能', emptyTitle: '来源库还是空的', emptySearchBody: '换一个关键词,或清空搜索查看全部来源。', emptyBody: '导入一个含 SKILL.md 的本地文件,它会作为可安装的来源出现在这里。', emptyFilterBody: '换一个分类或关键词,或清空筛选查看全部来源。', clearSearch: '清空搜索', clearFilters: '清空筛选', sourceFallback: '本地来源库 Skill。' }, tabs: { ariaLabel: '技能视图', market: '市场', builtin: '内置', installed: '已安装' }, - install: { action: (name) => `安装 ${name}`, installedTitle: '已安装到当前工作区', installed: '已安装', notInstalled: '未安装' }, + install: { action: (name) => `安装 ${name}`, installedAction: (name) => `${name} 已安装到当前工作区`, installedTitle: '已安装到当前工作区', installed: '已安装', notInstalled: '未安装' }, builtin: { ariaLabel: '内置技能', emptyTitle: '暂无内置技能', emptyBody: '应用自带的技能会出现在这里。', noMatchTitle: '没有匹配的内置技能', noMatchBody: '换一个关键词,或清空搜索查看全部内置技能。', fallback: '应用自带 Skill。', toolCount: (count: number) => `${count} 个工具` }, installed: { emptySearchTitle: '没有匹配的 Skill', emptyTitle: '等待添加 Skill', emptySearchBody: '换一个关键词,或清空搜索查看全部本地技能。', emptyBodyBeforeCode: '把一个含', emptyBodyAfterCode: '的文件夹放到工作区的 skills/ 目录下,刷新后会出现在这里。', refreshPending: '刷新中…', refresh: '刷新技能', listAriaLabel: '技能列表' }, context: { scope: { project: '项目', workspace: '工作区', user: '用户', custom: '自定义' }, decision: { advertised: '已进入上下文', disabled: '已停用', invalid: '元数据无效', host_incompatible: '主机不兼容', shadowed: '被高优先级覆盖', budget: '因预算省略' }, needsReview: '待确认', discoverySource: (scope, source) => `${scope}/${source} 发现源`, discoveryDiagnostic: { blocked_path: '路径被安全策略阻止', read_failed: '来源不可读取' } }, @@ -177,7 +178,7 @@ const SKILLS_COPY = { categories: { '内容创作': 'Content creation', '数据与AI': 'Data & AI', '设计与UI': 'Design & UI', 'DevOps与部署': 'DevOps & deployment', '文档与写作': 'Documents & writing', '效率工具': 'Productivity', '研究与分析': 'Research & analysis' }, market: { categoryAll: 'All categories', sortName: 'Sort: Name', sortRecent: 'Sort: Recent', controls: 'Marketplace filters and sorting', categoryFilter: 'Filter marketplace skills by category', sortAriaLabel: 'Marketplace skill sort order', ariaLabel: 'Skill marketplace', importLocal: 'Import local Skill', emptySearchTitle: 'No matching marketplace skills', emptyTitle: 'The source library is empty', emptySearchBody: 'Try another keyword or clear search to see all sources.', emptyBody: 'Import a local file containing SKILL.md to make it available as an installable source.', emptyFilterBody: 'Try another category or keyword, or clear the filters.', clearSearch: 'Clear search', clearFilters: 'Clear filters', sourceFallback: 'Local source-library Skill.' }, tabs: { ariaLabel: 'Skill views', market: 'Marketplace', builtin: 'Built in', installed: 'Installed' }, - install: { action: (name) => `Install ${name}`, installedTitle: 'Installed in this workspace', installed: 'Installed', notInstalled: 'Not installed' }, + install: { action: (name) => `Install ${name}`, installedAction: (name) => `${name} is installed in this workspace`, installedTitle: 'Installed in this workspace', installed: 'Installed', notInstalled: 'Not installed' }, builtin: { ariaLabel: 'Built-in skills', emptyTitle: 'No built-in skills', emptyBody: 'Skills included with the app appear here.', noMatchTitle: 'No matching built-in skills', noMatchBody: 'Try another keyword or clear search to see all built-in skills.', fallback: 'Skill included with the app.', toolCount: (count: number) => (count === 1 ? '1 tool' : `${count} tools`) }, installed: { emptySearchTitle: 'No matching Skills', emptyTitle: 'Waiting for a Skill', emptySearchBody: 'Try another keyword or clear search to see all local skills.', emptyBodyBeforeCode: 'Place a folder containing', emptyBodyAfterCode: 'in the workspace skills/ directory, then refresh to show it here.', refreshPending: 'Refreshing…', refresh: 'Refresh skills', listAriaLabel: 'Skill list' }, context: { scope: { project: 'Project', workspace: 'Workspace', user: 'User', custom: 'Custom' }, decision: { advertised: 'In context', disabled: 'Disabled', invalid: 'Invalid metadata', host_incompatible: 'Host incompatible', shadowed: 'Shadowed', budget: 'Budget omitted' }, needsReview: 'Needs review', discoverySource: (scope, source) => `${scope}/${source} discovery source`, discoveryDiagnostic: { blocked_path: 'Path blocked by the safety policy', read_failed: 'Source could not be read' } }, diff --git a/packages/ui/src/skills-panel.tsx b/packages/ui/src/skills-panel.tsx index 11cdb5e82c..d59ec59dfb 100644 --- a/packages/ui/src/skills-panel.tsx +++ b/packages/ui/src/skills-panel.tsx @@ -40,6 +40,7 @@ import { ICON_SIZE, Blocks, BookOpen, + Check, Download, FolderOpen, Loader2, @@ -334,6 +335,15 @@ export function SkillsModuleMain(props: { // ── Catalog rows (市场 / 内置): one job each — install. ────────────── function catalogInstallButton(key: string, name: string, installing: boolean, installed: boolean, onInstall?: () => void) { + const installIcon = installing + ?