Skip to content

Commit b5ea95b

Browse files
Merge pull request #153 from laststance/codex/update-deps-info-file-preview
Add rich file previews
2 parents 49beef4 + b5cf16b commit b5ea95b

14 files changed

Lines changed: 2979 additions & 908 deletions

package.json

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,15 @@
4545
"clsx": "^2.1.1",
4646
"electron-updater": "^6.8.3",
4747
"lucide-react": "^1.14.0",
48-
"react": "^19.2.5",
49-
"react-dom": "^19.2.5",
48+
"react": "^19.2.6",
49+
"react-dom": "^19.2.6",
5050
"react-grid-layout": "^2.2.3",
51+
"react-markdown": "^10.1.0",
5152
"react-redux": "^9.2.0",
5253
"react-resizable-panels": "^4.11.0",
5354
"react-window": "^2.2.7",
55+
"remark-gfm": "^4.0.1",
56+
"shiki": "^4.0.2",
5457
"sonner": "^2.0.7",
5558
"tailwind-merge": "^3.5.0",
5659
"ts-pattern": "^5.9.0",
@@ -63,33 +66,33 @@
6366
"@storybook/addon-a11y": "10.3.6",
6467
"@storybook/addon-docs": "10.3.6",
6568
"@storybook/react-vite": "10.3.6",
66-
"@tailwindcss/vite": "^4.2.4",
69+
"@tailwindcss/vite": "^4.3.0",
6770
"@types/culori": "^4.0.1",
68-
"@types/node": "^25.6.0",
71+
"@types/node": "^25.6.2",
6972
"@types/react": "^19.2.14",
7073
"@types/react-dom": "^19.2.3",
7174
"@vitejs/plugin-react": "^6.0.1",
7275
"@vitest/browser-playwright": "^4.1.5",
7376
"@vitest/coverage-v8": "4.1.5",
7477
"code-inspector-plugin": "^1.5.1",
7578
"culori": "^4.0.2",
76-
"electron": "^41.5.0",
79+
"electron": "^42.0.1",
7780
"electron-builder": "^26.8.1",
7881
"electron-vite": "^5.0.0",
7982
"eslint": "^10.3.0",
8083
"eslint-config-ts-prefixer": "^4.2.0",
81-
"eslint-plugin-react-you-might-not-need-an-effect": "^0.10.0",
82-
"fallow": "2.65.0",
84+
"eslint-plugin-react-you-might-not-need-an-effect": "^0.10.1",
85+
"fallow": "2.69.0",
8386
"husky": "^9.1.7",
84-
"lint-staged": "^16.4.0",
87+
"lint-staged": "^17.0.3",
8588
"npm-run-all2": "^8.0.4",
8689
"playwright": "^1.59.1",
8790
"prettier": "^3.8.3",
8891
"sharp": "^0.34.5",
8992
"storybook": "10.3.6",
90-
"tailwindcss": "^4.2.4",
93+
"tailwindcss": "^4.3.0",
9194
"typescript": "^6.0.3",
92-
"vite": "^8.0.10",
95+
"vite": "^8.0.11",
9396
"vitest": "^4.1.5",
9497
"vitest-browser-react": "^2.2.0"
9598
},

pnpm-lock.yaml

Lines changed: 1881 additions & 863 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/renderer/src/components/SkipToMainContentLink.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const SkipToMainContentLink = memo(
3838
href="#main-content"
3939
className="sr-only focus:not-sr-only focus:fixed focus:top-0 focus:left-0 focus:z-50 focus:bg-primary focus:text-primary-foreground focus:p-2"
4040
>
41-
[Press Enter] Skip forcus to main content
41+
Skip to main content
4242
</a>
4343
)
4444
},

src/renderer/src/components/sidebar/BookmarkDetailModal.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Button } from '@/renderer/src/components/ui/button'
55
import {
66
Dialog,
77
DialogContent,
8+
DialogDescription,
89
DialogHeader,
910
DialogTitle,
1011
} from '@/renderer/src/components/ui/dialog'
@@ -96,6 +97,10 @@ export const BookmarkDetailModal = React.memo(
9697
<DialogTitle className="text-base font-semibold">
9798
{bookmark.name}
9899
</DialogTitle>
100+
<DialogDescription className="sr-only">
101+
Bookmark details for {bookmark.name}. Review install status,
102+
open the source repository, or remove this bookmark.
103+
</DialogDescription>
99104
</DialogHeader>
100105

101106
{bookmark.repo ? (

src/renderer/src/components/sidebar/SourceCard.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,11 @@ export const SourceCard = React.memo(function SourceCard(): React.ReactElement {
161161
variant="ghost"
162162
size="icon"
163163
className="h-11 w-11"
164+
aria-label={
165+
isRefreshing
166+
? 'Refreshing skills and agent status'
167+
: 'Refresh skills and agent status'
168+
}
164169
onClick={(e) => {
165170
e.stopPropagation()
166171
handleRefresh()

src/renderer/src/components/skills/CodePreview.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ interface CodePreviewProps {
2323
* Root here — rather than inside `FileTabs` — is what makes the panel
2424
* discoverable to assistive tech.
2525
*
26-
* All syntax highlighting was removed on purpose: plain monospace text reads
27-
* fine across every language we support and keeps the renderer bundle small.
26+
* FileContent owns the actual rendering mode: source-like files get Shiki
27+
* syntax highlighting, and Markdown can switch into a rendered reading view.
2828
*/
2929
export const CodePreview = React.memo(function CodePreview({
3030
skillPath,
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { render } from 'vitest-browser-react'
3+
4+
import type { PreviewContent } from '@/renderer/src/hooks/useCodePreview'
5+
6+
/**
7+
* Build a text preview payload without pulling in the IPC hook.
8+
* @param overrides - File metadata/content fields relevant to the preview.
9+
* @returns PreviewContent for FileContent's `text` branch.
10+
*/
11+
function makeTextContent(
12+
overrides: Partial<PreviewContent & { content: string }> = {},
13+
): PreviewContent {
14+
const content = 'content' in overrides ? overrides.content : '# Skill\n'
15+
return {
16+
kind: 'text',
17+
data: {
18+
name: 'SKILL.md',
19+
content: content ?? '# Skill\n',
20+
extension: '.md',
21+
lineCount: content?.split('\n').length ?? 1,
22+
},
23+
}
24+
}
25+
26+
describe('FileContent Markdown modes', () => {
27+
it('renders Markdown files in code mode first, then switches to Reading Mode', async () => {
28+
const { FileContent } = await import('./FileContent')
29+
const screen = await render(
30+
<FileContent
31+
content={makeTextContent({
32+
content:
33+
'---\nname: install\n---\n# Install\n\n- [x] Link agents\n\n```ts\nconst ok = true\n```',
34+
})}
35+
/>,
36+
)
37+
38+
await expect
39+
.element(screen.getByRole('radio', { name: /Show Markdown source/i }))
40+
.toBeInTheDocument()
41+
expect(screen.getByRole('heading', { name: 'Install' }).query()).toBeNull()
42+
43+
await screen.getByRole('radio', { name: /Show rendered Markdown/i }).click()
44+
45+
await expect
46+
.element(screen.getByRole('heading', { name: 'Install' }))
47+
.toBeInTheDocument()
48+
await expect.element(screen.getByText('Link agents')).toBeInTheDocument()
49+
expect(screen.getByText('name: install').query()).toBeNull()
50+
})
51+
52+
it('keeps Markdown that starts with a horizontal rule in Reading Mode', async () => {
53+
const { FileContent } = await import('./FileContent')
54+
const screen = await render(
55+
<FileContent
56+
content={makeTextContent({
57+
content: '---\n# Keep Me\n---\n\nVisible body',
58+
})}
59+
/>,
60+
)
61+
62+
await screen.getByRole('radio', { name: /Show rendered Markdown/i }).click()
63+
64+
await expect
65+
.element(screen.getByRole('heading', { name: 'Keep Me' }))
66+
.toBeInTheDocument()
67+
await expect.element(screen.getByText('Visible body')).toBeInTheDocument()
68+
})
69+
70+
it('renders language-less code fences as block code without AST attributes', async () => {
71+
const { FileContent } = await import('./FileContent')
72+
const screen = await render(
73+
<FileContent
74+
content={makeTextContent({
75+
content:
76+
'# Skill\n\n```\nnpx skills list --json\n```\n\nInline `skill` stays compact.',
77+
})}
78+
/>,
79+
)
80+
81+
await screen.getByRole('radio', { name: /Show rendered Markdown/i }).click()
82+
83+
const blockCode = screen.getByText(/npx skills list --json/).query()
84+
const inlineCode = screen.getByText('skill', { exact: true }).query()
85+
86+
expect(blockCode).toBeInstanceOf(HTMLElement)
87+
expect(inlineCode).toBeInstanceOf(HTMLElement)
88+
expect(blockCode?.closest('pre')).toBeInstanceOf(HTMLPreElement)
89+
expect(inlineCode?.closest('pre')).toBeNull()
90+
expect(screen.container.querySelector('[node]')).toBeNull()
91+
})
92+
93+
it('renders language-tagged code fences as block code', async () => {
94+
const { FileContent } = await import('./FileContent')
95+
const screen = await render(
96+
<FileContent
97+
content={makeTextContent({
98+
content: '# Skill\n\n```ts\nconst ok = true\n```',
99+
})}
100+
/>,
101+
)
102+
103+
await screen.getByRole('radio', { name: /Show rendered Markdown/i }).click()
104+
105+
const blockCode = screen.getByText(/const ok = true/).query()
106+
107+
expect(blockCode).toBeInstanceOf(HTMLElement)
108+
expect(blockCode?.closest('pre')).toBeInstanceOf(HTMLPreElement)
109+
})
110+
111+
it('locks Reading Mode to vertical scrolling when Markdown is wider than the pane', async () => {
112+
const { FileContent } = await import('./FileContent')
113+
const wideInline = 'very-long-inline-token-'.repeat(30)
114+
const wideBlock = 'wide command '.repeat(40)
115+
const screen = await render(
116+
<FileContent
117+
content={makeTextContent({
118+
content: `# Wide\n\n\`${wideInline}\`\n\n\`\`\`\n${wideBlock}\n\`\`\``,
119+
})}
120+
/>,
121+
)
122+
123+
await screen.getByRole('radio', { name: /Show rendered Markdown/i }).click()
124+
125+
const scrollPane = screen.container.querySelector(
126+
'[data-markdown-reading-scroll]',
127+
)
128+
expect(scrollPane).toBeInstanceOf(HTMLElement)
129+
const pane = scrollPane as HTMLElement
130+
131+
// Programmatic scroll mirrors trackpad horizontal gestures in the renderer.
132+
expect(pane.scrollWidth).toBeGreaterThan(pane.clientWidth)
133+
pane.scrollTo({ left: 240 })
134+
expect(pane.scrollLeft).toBe(0)
135+
})
136+
137+
it('adds a bottom spacer after source code so the final line can breathe', async () => {
138+
const { FileContent } = await import('./FileContent')
139+
const screen = await render(
140+
<FileContent
141+
content={makeTextContent({
142+
content: Array.from(
143+
{ length: 48 },
144+
(_, index) => `line ${index + 1}`,
145+
).join('\n'),
146+
})}
147+
/>,
148+
)
149+
150+
const scrollPane = screen.container.querySelector(
151+
'[data-file-preview-scroll]',
152+
)
153+
const spacer = screen.container.querySelector(
154+
'[data-file-preview-bottom-spacer]',
155+
)
156+
157+
expect(scrollPane).toBeInstanceOf(HTMLElement)
158+
expect(spacer).toBeInstanceOf(HTMLElement)
159+
expect(scrollPane?.lastElementChild).toBe(spacer)
160+
})
161+
})

0 commit comments

Comments
 (0)