From f82b596fbbdbfe75d86357a820fcbf8821f9e4d3 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, 25 Jul 2026 16:23:52 +0800 Subject: [PATCH] fix(desktop): open installed skill details from the market overview Clicking an installed skill in the Skills Market only wrote skillStore.selectedSkill, but the page renders details exclusively off marketStore.selectedId, so the click had no visible effect. Render the shared SkillDetail when an installed selection exists, mirroring the Settings -> Skills wiring: hidden (not unmounted) market home to preserve search/collapse state, and the same active-project context guard that drops stale details. Follow-up to #1098. --- desktop/src/pages/Market.test.tsx | 62 +++++++++++++++++++++++++++++++ desktop/src/pages/Market.tsx | 42 +++++++++++++++++---- 2 files changed, 97 insertions(+), 7 deletions(-) diff --git a/desktop/src/pages/Market.test.tsx b/desktop/src/pages/Market.test.tsx index cc5dd497..42a96cab 100644 --- a/desktop/src/pages/Market.test.tsx +++ b/desktop/src/pages/Market.test.tsx @@ -10,7 +10,9 @@ vi.mock('../components/markdown/MarkdownRenderer', () => ({ import { useMarketStore } from '../stores/marketStore' import { useSettingsStore } from '../stores/settingsStore' +import { useSkillStore } from '../stores/skillStore' import type { NormalizedSkill, NormalizedSkillDetail } from '../types/market' +import type { SkillDetail as LocalSkillDetail } from '../types/skill' import { Market } from './Market' function makeSkill(overrides: Partial = {}): NormalizedSkill { @@ -40,9 +42,32 @@ function makeDetail(overrides: Partial = {}): NormalizedS } } +function makeLocalSkillDetail(overrides: Partial = {}): LocalSkillDetail { + return { + meta: { + name: 'local-demo', + displayName: 'Local Demo', + description: 'An installed skill', + source: 'project', + userInvocable: true, + contentLength: 12, + hasDirectory: true, + }, + tree: [], + files: [], + skillRoot: '/tmp/local-demo', + ...overrides, + } +} + beforeEach(() => { localStorage.clear() useSettingsStore.setState({ locale: 'en' }) + useSkillStore.setState({ + selectedSkill: null, + selectedSkillContext: null, + isDetailLoading: false, + }) const detail = makeDetail() useMarketStore.setState({ items: [makeSkill()], @@ -108,4 +133,41 @@ describe('Market', () => { '…/skills/demo/', ) }) + + it('opens the installed skill detail selected from the overview and returns to the market home', async () => { + render() + + act(() => { + useSkillStore.setState({ + selectedSkill: makeLocalSkillDetail(), + selectedSkillReturnTab: 'skills', + selectedSkillContext: '', + }) + }) + + expect(await screen.findByTestId('skill-detail-view')).toBeInTheDocument() + expect(screen.getByRole('heading', { name: 'Local Demo', level: 1 })).toBeInTheDocument() + + fireEvent.click(screen.getByRole('button', { name: 'Back to list' })) + await waitFor(() => expect(screen.queryByTestId('skill-detail-view')).not.toBeInTheDocument()) + expect(screen.getByRole('button', { name: 'Demo Skill' })).toBeInTheDocument() + }) + + it('keeps the market detail view authoritative when a market skill is selected', async () => { + render() + + fireEvent.click(screen.getByRole('button', { name: 'Demo Skill' })) + expect(await screen.findByRole('heading', { name: 'Demo Skill', level: 1 })).toBeInTheDocument() + + act(() => { + useSkillStore.setState({ + selectedSkill: makeLocalSkillDetail(), + selectedSkillReturnTab: 'skills', + selectedSkillContext: '', + }) + }) + + expect(screen.queryByRole('heading', { name: 'Local Demo' })).not.toBeInTheDocument() + expect(screen.getByRole('heading', { name: 'Demo Skill', level: 1 })).toBeInTheDocument() + }) }) diff --git a/desktop/src/pages/Market.tsx b/desktop/src/pages/Market.tsx index 682d5bf0..5b8826dc 100644 --- a/desktop/src/pages/Market.tsx +++ b/desktop/src/pages/Market.tsx @@ -1,12 +1,14 @@ import { useEffect, useRef, useState } from 'react' import { useTranslation } from '../i18n' import { useMarketStore } from '../stores/marketStore' +import { useSessionStore } from '../stores/sessionStore' import { useSkillStore } from '../stores/skillStore' import { useUIStore } from '../stores/uiStore' import { InstallConfirmDialog } from '../components/market/InstallConfirmDialog' import { MarketHome } from '../components/market/MarketHome' import { MarketSkillDetail } from '../components/market/MarketSkillDetail' import { ConfirmDialog } from '../components/shared/ConfirmDialog' +import { SkillDetail } from '../components/skills/SkillDetail' import type { NormalizedSkill } from '../types/market' function focusSkillControl(id: string, preferred: 'action' | 'open' = 'action') { @@ -28,6 +30,15 @@ export function Market() { const t = useTranslation() const selectedId = useMarketStore((s) => s.selectedId) const installingIds = useMarketStore((s) => s.installingIds) + // InstalledSkillsOverview 点击已装技能只写 skillStore.selectedSkill, + // 这里把它接成真正的详情视图,否则点击没有任何界面反馈 + const selectedInstalledSkill = useSkillStore((s) => s.selectedSkill) + const selectedInstalledContext = useSkillStore((s) => s.selectedSkillContext) + const isInstalledDetailLoading = useSkillStore((s) => s.isDetailLoading) + const sessions = useSessionStore((s) => s.sessions) + const activeSessionId = useSessionStore((s) => s.activeSessionId) + const currentSkillContext = sessions.find((session) => session.id === activeSessionId)?.workDir ?? '' + const showInstalledDetail = Boolean(selectedInstalledSkill) || isInstalledDetailLoading const [confirmInstall, setConfirmInstall] = useState(null) const [confirmUninstall, setConfirmUninstall] = useState(null) const lastOpenedIdRef = useRef(null) @@ -40,6 +51,17 @@ export function Market() { previousSelectedIdRef.current = selectedId }, [selectedId]) + // 与 Settings → Skills 一致:活跃项目切换后,丢弃上一个项目上下文的详情 + useEffect(() => { + if ( + (selectedInstalledSkill || isInstalledDetailLoading) + && selectedInstalledContext !== null + && selectedInstalledContext !== currentSkillContext + ) { + useSkillStore.getState().clearSelection() + } + }, [currentSkillContext, isInstalledDetailLoading, selectedInstalledContext, selectedInstalledSkill]) + const findSkill = (id: string): NormalizedSkill | null => { const state = useMarketStore.getState() if (state.detail?.id === id) return state.detail @@ -108,13 +130,19 @@ export function Market() { {selectedId ? ( ) : ( - { - lastOpenedIdRef.current = id - void useMarketStore.getState().openDetail(id) - }} - /> + <> + {showInstalledDetail && } + {/* hidden 而非卸载:保住 MarketHome/InstalledSkillsOverview 的搜索与折叠状态 */} + + )}