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 的搜索与折叠状态 */} + + )}