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.
This commit is contained in:
程序员阿江(Relakkes) 2026-07-25 16:23:52 +08:00
parent 441979aa2d
commit f82b596fbb
2 changed files with 97 additions and 7 deletions

View File

@ -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> = {}): NormalizedSkill {
@ -40,9 +42,32 @@ function makeDetail(overrides: Partial<NormalizedSkillDetail> = {}): NormalizedS
}
}
function makeLocalSkillDetail(overrides: Partial<LocalSkillDetail> = {}): 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(<Market />)
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(<Market />)
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()
})
})

View File

@ -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<NormalizedSkill | null>(null)
const [confirmUninstall, setConfirmUninstall] = useState<NormalizedSkill | null>(null)
const lastOpenedIdRef = useRef<string | null>(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 ? (
<MarketSkillDetail onRequestInstall={requestInstall} onRequestUninstall={requestUninstall} />
) : (
<MarketHome
onRequestInstall={requestInstall}
onOpenSkill={(id) => {
lastOpenedIdRef.current = id
void useMarketStore.getState().openDetail(id)
}}
/>
<>
{showInstalledDetail && <SkillDetail />}
{/* hidden 而非卸载:保住 MarketHome/InstalledSkillsOverview 的搜索与折叠状态 */}
<div hidden={showInstalledDetail} className="flex min-h-0 flex-1 flex-col">
<MarketHome
onRequestInstall={requestInstall}
onOpenSkill={(id) => {
lastOpenedIdRef.current = id
void useMarketStore.getState().openDetail(id)
}}
/>
</div>
</>
)}
<InstallConfirmDialog