Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion web/e2e/skill-detail-relative-links.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ test.describe('Skill Detail Relative Links (Real API)', () => {
extraFiles: [
{
path: 'docs/usage.md',
content: '# Usage\n\nThis is linked documentation.',
content: '# Usage\n\nThis is linked documentation.\n\n[Nested](nested.md)',
},
{
path: 'docs/nested.md',
content: '# Nested\n\nSecond-level linked documentation.',
}
],
})

Expand All @@ -40,6 +44,11 @@ test.describe('Skill Detail Relative Links (Real API)', () => {
await page.getByRole('link', { name: 'Usage' }).click()
await expect(page.getByRole('dialog')).toContainText('usage.md')
await expect(page.getByRole('dialog')).toContainText('This is linked documentation.')
await expect(page.getByRole('dialog').getByRole('link', { name: 'Nested' })).toBeVisible()

await page.getByRole('dialog').getByRole('link', { name: 'Nested' }).click()
await expect(page.getByRole('dialog')).toContainText('nested.md')
await expect(page.getByRole('dialog')).toContainText('Second-level linked documentation.')

await page.getByRole('button', { name: 'Close' }).click()
await expect(page.getByRole('dialog')).toBeHidden()
Expand Down
6 changes: 4 additions & 2 deletions web/src/features/skill/file-preview-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react'
import { useState, type MouseEvent } from 'react'
import { Copy, Check, Download, X } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Dialog, DialogContent } from '@/shared/ui/dialog'
Expand All @@ -18,6 +18,7 @@ interface FilePreviewDialogProps {
isLoading: boolean
error: Error | null
onDownload: () => void
onLinkClick?: (href: string, event: MouseEvent<HTMLAnchorElement>) => void
}

/**
Expand All @@ -33,6 +34,7 @@ export function FilePreviewDialog({
isLoading,
error,
onDownload,
onLinkClick,
}: FilePreviewDialogProps) {
const { t } = useTranslation()
// Tracks the copy animation state: idle → spinning → done
Expand Down Expand Up @@ -143,7 +145,7 @@ export function FilePreviewDialog({
<Button onClick={onDownload}>{t('filePreview.downloadHint', { name: node.name })}</Button>
</div>
) : content && isMarkdown ? (
<MarkdownRenderer content={content} />
<MarkdownRenderer content={content} onLinkClick={onLinkClick} />
) : content && shouldHighlight ? (
<CodeRenderer code={content} language={language} />
) : content ? (
Expand Down
42 changes: 40 additions & 2 deletions web/src/pages/skill-detail.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,27 @@ vi.mock('@/features/skill/markdown-renderer', () => ({
}))

vi.mock('@/features/skill/file-preview-dialog', () => ({
FilePreviewDialog: ({ open, node }: { open: boolean; node: { path: string } | null }) => (
open && node ? <div role="dialog">preview:{node.path}</div> : null
FilePreviewDialog: ({
open,
node,
onLinkClick,
}: {
open: boolean
node: { path: string } | null
onLinkClick?: (href: string, event: MouseEvent<HTMLAnchorElement>) => void
}) => (
open && node
? (
<div role="dialog">
preview:{node.path}
{onLinkClick && (
<a href="nested.md" onClick={(event) => onLinkClick('nested.md', event)}>
Nested
</a>
)}
</div>
)
: null
),
}))

Expand Down Expand Up @@ -490,6 +509,25 @@ describe('SkillDetailPage', () => {
expect(toastMocks.error).not.toHaveBeenCalled()
})

it('resolves links inside previewed markdown files against the previewed file path', () => {
useSkillFilesMock.mockReturnValue({
data: [
createSkillFile('README.md'),
createSkillFile('docs/usage.md'),
createSkillFile('docs/nested.md'),
],
})

render(<SkillDetailPage />)
fireEvent.click(screen.getByRole('link', { name: 'Usage' }))
expect(screen.getByRole('dialog').textContent).toContain('preview:docs/usage.md')

fireEvent.click(screen.getByRole('link', { name: 'Nested' }))

expect(screen.getByRole('dialog').textContent).toContain('preview:docs/nested.md')
expect(toastMocks.error).not.toHaveBeenCalled()
})

it('keeps the viewer on the detail page and shows a toast for missing package files', () => {
useSkillFilesMock.mockReturnValue({
data: [
Expand Down
17 changes: 15 additions & 2 deletions web/src/pages/skill-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,12 @@ export function SkillDetailPage() {
setPreviewDialogOpen(true)
}

const handleOverviewLinkClick = (href: string, event: MouseEvent<HTMLAnchorElement>) => {
const resolution = resolvePackageRelativeLink(href, documentationPath, files)
const handlePackageMarkdownLinkClick = (
href: string,
event: MouseEvent<HTMLAnchorElement>,
currentFilePath: string | null | undefined,
) => {
const resolution = resolvePackageRelativeLink(href, currentFilePath, files)

if (resolution.status === 'ignored') {
return
Expand All @@ -318,6 +322,14 @@ export function SkillDetailPage() {
toast.error(t('skillDetail.packageLinkMissingTitle'), t('skillDetail.packageLinkMissingDescription'))
}

const handleOverviewLinkClick = (href: string, event: MouseEvent<HTMLAnchorElement>) => {
handlePackageMarkdownLinkClick(href, event, documentationPath)
}

const handlePreviewLinkClick = (href: string, event: MouseEvent<HTMLAnchorElement>) => {
handlePackageMarkdownLinkClick(href, event, previewNode?.path)
}

// Download a single file from the skill version
const handleDownloadFile = () => {
const isAnonymousAllowed = namespace === 'global' && skill?.visibility === 'PUBLIC'
Expand Down Expand Up @@ -1652,6 +1664,7 @@ export function SkillDetailPage() {
isLoading={isLoadingPreview}
error={previewError}
onDownload={handleDownloadFile}
onLinkClick={handlePreviewLinkClick}
/>
</div>
)
Expand Down
Loading