From 0871adf6a897f205b442c80cd4aaeb7807584b8b Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Fri, 26 Jun 2026 14:12:09 -0400 Subject: [PATCH] Align command palette icons and shortcuts (#2057) * fix command palette icons and shortcuts * Address command palette review feedback * increase limit * upd * fix(app): restore media-aware file entry icons * knip * fix(app): address command palette review GitOrigin-RevId: 08f373031616ce86e331db976240a1dd20cdd94c --- .../components/CommandPalette.dom.test.tsx | 117 ++++++++++++++++ .../app/src/components/CommandPalette.tsx | 17 ++- .../ComposerContextChips.dom.test.tsx | 24 +++- .../src/components/ComposerContextChips.tsx | 10 +- packages/app/src/components/FileTree.tsx | 7 +- .../components/command-palette-search.test.ts | 37 +++-- .../src/components/command-palette-search.ts | 31 ++-- .../src/components/file-entry-icon.test.tsx | 34 +++++ .../app/src/components/file-entry-icon.tsx | 132 ++++++++++++++++++ .../editor/ComposerMentionInput.dom.test.tsx | 28 ++-- .../composer-mention/ComposerMentionMenu.tsx | 13 +- .../composer-mention/composer-mention.ts | 4 +- .../app/src/editor/link-path-suggestions.tsx | 21 ++- .../src/editor/registry/lucide-svg.test.ts | 14 +- .../app/src/editor/registry/lucide-svg.ts | 20 ++- .../app/src/lib/keyboard-shortcuts.test.ts | 19 +++ packages/app/src/lib/keyboard-shortcuts.ts | 8 ++ packages/app/src/locales/en/messages.json | 1 + packages/app/src/locales/en/messages.po | 5 + packages/app/src/locales/pseudo/messages.json | 1 + packages/app/src/locales/pseudo/messages.po | 5 + 21 files changed, 467 insertions(+), 81 deletions(-) create mode 100644 packages/app/src/components/file-entry-icon.test.tsx create mode 100644 packages/app/src/components/file-entry-icon.tsx diff --git a/packages/app/src/components/CommandPalette.dom.test.tsx b/packages/app/src/components/CommandPalette.dom.test.tsx index cd3ae1923..3bf433a15 100644 --- a/packages/app/src/components/CommandPalette.dom.test.tsx +++ b/packages/app/src/components/CommandPalette.dom.test.tsx @@ -280,6 +280,12 @@ describe('CommandPalette DOM behavior', () => { expect(switchProject.querySelector('svg[aria-hidden="true"]')).not.toBeNull(); expect(document.body.textContent).not.toContain('Start fresh in a new folder'); + expect(screen.getByTestId('command-palette-new-file').textContent).toMatch(/⌘ N|Ctrl N/); + expect(screen.getByTestId('command-palette-new-folder').textContent).toMatch( + /⇧⌘ N|Ctrl Shift N/, + ); + expect(screen.getByTestId('command-palette-open-folder').textContent).toMatch(/⌘ O|Ctrl O/); + fireEvent.click(switchProject); await waitFor(() => expect(bridge.navigator.open).toHaveBeenCalledTimes(1)); @@ -309,6 +315,15 @@ describe('CommandPalette DOM behavior', () => { expect(screen.queryByTestId('command-palette-switch-project')).toBeNull(); }); + test('new-folder shortcut is desktop-only while new-file shortcut is always visible', async () => { + await renderPalette({ bridge: null }); + + expect(screen.getByTestId('command-palette-new-file').textContent).toMatch(/⌘ N|Ctrl N/); + expect(screen.getByTestId('command-palette-new-folder').textContent).not.toMatch( + /⇧⌘ N|Ctrl Shift N/, + ); + }); + test('settings command is searchable by preferences/config, closes the palette, and routes through the canonical hash', async () => { const { onOpenChange } = await renderPalette({ bridge: null }); @@ -561,4 +576,106 @@ describe('NavigationItem path subtitle', () => { expect(rowA.textContent).toContain('reports/q3/data.csv'); expect(rowB.textContent).toContain('exports/legacy/data.csv'); }); + + test('file and folder rows render sidebar-aligned icons and extension badges', async () => { + const { NavigationItem } = await import('./CommandPalette'); + render( + <> + {}} + /> + {}} + /> + {}} + /> + {}} + /> + {}} + /> + {}} + /> + {}} + /> + {}} + /> + , + ); + + const markdownRow = screen.getByTestId('command-palette-nav-file-notes/readme'); + expect(markdownRow.querySelector('[data-testid="file-entry-icon-markdown"]')).not.toBeNull(); + expect(markdownRow.querySelector('[data-testid="file-entry-extension-badge"]')).toBeNull(); + + const mdxRow = screen.getByTestId('command-palette-nav-file-docs/component'); + expect(mdxRow.querySelector('[data-testid="file-entry-icon-markdown"]')).not.toBeNull(); + expect(mdxRow.textContent).toContain('MDX'); + + const pngRow = screen.getByTestId('command-palette-nav-file-assets/photo.png'); + expect(pngRow.querySelector('[data-testid="file-entry-icon-image"]')).not.toBeNull(); + expect(pngRow.textContent).toContain('PNG'); + + const videoRow = screen.getByTestId('command-palette-nav-file-media/demo.mp4'); + expect(videoRow.querySelector('[data-testid="file-entry-icon-video"]')).not.toBeNull(); + expect(videoRow.textContent).toContain('MP4'); + + const audioRow = screen.getByTestId('command-palette-nav-file-audio/theme.mp3'); + expect(audioRow.querySelector('[data-testid="file-entry-icon-audio"]')).not.toBeNull(); + expect(audioRow.textContent).toContain('MP3'); + + const genericFileRow = screen.getByTestId('command-palette-nav-file-src/index.ts'); + expect(genericFileRow.querySelector('[data-testid="file-entry-icon-file"]')).not.toBeNull(); + expect(genericFileRow.querySelector('[data-testid="file-entry-icon-image"]')).toBeNull(); + expect(genericFileRow.textContent).toContain('TS'); + + const recentPngRow = screen.getByTestId('command-palette-nav-file-recents/screenshot.png'); + expect(recentPngRow.querySelector('[data-testid="file-entry-icon-image"]')).not.toBeNull(); + expect(recentPngRow.querySelector('[data-testid="file-entry-icon-markdown"]')).toBeNull(); + + const folderRow = screen.getByTestId('command-palette-nav-folder-docs'); + expect(folderRow.querySelector('[data-file-entry-icon="folder"]')).not.toBeNull(); + expect(folderRow.querySelector('[data-testid="file-entry-extension-badge"]')).toBeNull(); + }); }); diff --git a/packages/app/src/components/CommandPalette.tsx b/packages/app/src/components/CommandPalette.tsx index 827e0946a..b2b1b8cee 100644 --- a/packages/app/src/components/CommandPalette.tsx +++ b/packages/app/src/components/CommandPalette.tsx @@ -57,6 +57,7 @@ import { type TagDocEntry, } from '@/components/command-palette-tag-search'; import { requestDocPanelTab } from '@/components/doc-panel-events'; +import { FileEntryIcon } from '@/components/file-entry-icon'; import { defaultInitialDir } from '@/components/file-tree-utils'; import { NewItemDialog } from '@/components/NewItemDialog'; import { usePageList } from '@/components/PageListContext'; @@ -72,7 +73,6 @@ import { } from '@/components/ui/command'; import { useDocumentContext } from '@/editor/DocumentContext'; import type { TagSummaryEntry } from '@/editor/extensions/tag-suggestion'; -import { getFileIcon } from '@/editor/registry/file-icons'; import { useIsEmbedded } from '@/hooks/use-is-embedded'; import { useSemanticSearchStatus } from '@/hooks/use-semantic-search-status'; import type { OkDesktopBridge, RecentProjectEntry } from '@/lib/desktop-bridge-types'; @@ -128,10 +128,11 @@ export function NavigationItem({ onSelect: () => void; disabled?: boolean; }) { - const Icon = getFileIcon(entry); const title = 'title' in entry && entry.title ? entry.title : (entry.path.split('/').pop() ?? entry.path); const snippet = 'snippet' in entry ? entry.snippet : undefined; + const docExt = 'docExt' in entry ? entry.docExt : undefined; + const bodyIndexed = 'bodyIndexed' in entry ? entry.bodyIndexed : undefined; return ( - +
@@ -1043,6 +1050,7 @@ export function CommandPalette({ bridge = null, open, onOpenChange }: CommandPal New file + {formatShortcut('new-item')} ) : null} {showCreateFolder ? ( @@ -1058,6 +1066,9 @@ export function CommandPalette({ bridge = null, open, onOpenChange }: CommandPal New folder + {bridge ? ( + {formatShortcut('new-folder')} + ) : null} ) : null} {showGraphCommand ? ( diff --git a/packages/app/src/components/ComposerContextChips.dom.test.tsx b/packages/app/src/components/ComposerContextChips.dom.test.tsx index 75fbfe173..77d6f3509 100644 --- a/packages/app/src/components/ComposerContextChips.dom.test.tsx +++ b/packages/app/src/components/ComposerContextChips.dom.test.tsx @@ -37,15 +37,33 @@ describe('ComposerContextChips', () => { expect(chip.querySelectorAll('button').length).toBe(1); }); - test('the leading icon is type-aware: a folder chip shows a different glyph than a page chip', async () => { - await renderChips(['specs/foo', 'notes.md']); + test('the leading icon is type-aware across folders, pages, video, and audio', async () => { + await renderChips(['specs/foo', 'notes.md', 'clips/demo.mp4', 'audio/theme.mp3']); const folderChip = screen.getByTestId('composer-context-chip-file-specs/foo'); const pageChip = screen.getByTestId('composer-context-chip-file-notes.md'); + const videoChip = screen.getByTestId('composer-context-chip-file-clips/demo.mp4'); + const audioChip = screen.getByTestId('composer-context-chip-file-audio/theme.mp3'); + expect( + folderChip.querySelector('button > span:first-child [data-file-entry-icon="folder"]'), + ).not.toBeNull(); + expect( + pageChip.querySelector('button > span:first-child [data-testid="file-entry-icon-markdown"]'), + ).not.toBeNull(); + expect( + videoChip.querySelector('button > span:first-child [data-testid="file-entry-icon-video"]'), + ).not.toBeNull(); + expect( + audioChip.querySelector('button > span:first-child [data-testid="file-entry-icon-audio"]'), + ).not.toBeNull(); const folderIcon = folderChip.querySelector('button svg')?.outerHTML; const pageIcon = pageChip.querySelector('button svg')?.outerHTML; + const videoIcon = videoChip.querySelector('button svg')?.outerHTML; + const audioIcon = audioChip.querySelector('button svg')?.outerHTML; expect(folderIcon).toBeDefined(); expect(pageIcon).toBeDefined(); - expect(folderIcon).not.toBe(pageIcon); + expect(videoIcon).toBeDefined(); + expect(audioIcon).toBeDefined(); + expect(new Set([folderIcon, pageIcon, videoIcon, audioIcon]).size).toBe(4); }); test('renders nothing when the file set is empty', async () => { diff --git a/packages/app/src/components/ComposerContextChips.tsx b/packages/app/src/components/ComposerContextChips.tsx index 071fb3252..3b102c9c9 100644 --- a/packages/app/src/components/ComposerContextChips.tsx +++ b/packages/app/src/components/ComposerContextChips.tsx @@ -1,8 +1,8 @@ import { useLingui } from '@lingui/react/macro'; import { X } from 'lucide-react'; import type { ReactNode } from 'react'; +import { FileEntryPathIcon } from '@/components/file-entry-icon'; import { Button } from '@/components/ui/button'; -import { getFileIcon, mentionPathToDescriptor } from '@/editor/registry/file-icons'; import { cn } from '@/lib/utils'; /** Last path segment of a workspace-relative path — the compact chip label @@ -17,7 +17,6 @@ function FileChip({ path, onRemove }: { path: string; onRemove: () => void }) { const { t } = useLingui(); const label = chipBasename(path); const removeLabel = t`Remove ${label} from context`; - const FileIcon = getFileIcon(mentionPathToDescriptor(path)); return ( void }) { }} className="group/remove relative size-3.5 shrink-0 rounded-sm text-muted-foreground/80 hover:text-foreground" > - + + + `; +const MARKDOWN_FILE_ICON_SYMBOL = ``; type IconNode = [string, Record][]; diff --git a/packages/app/src/components/command-palette-search.test.ts b/packages/app/src/components/command-palette-search.test.ts index 93c555ac0..75e68ec06 100644 --- a/packages/app/src/components/command-palette-search.test.ts +++ b/packages/app/src/components/command-palette-search.test.ts @@ -47,8 +47,6 @@ describe('buildWorkspaceEntries', () => { path: 'data/example.csv', name: 'example.csv', bodyIndexed: false, - assetExt: 'csv', - mediaKind: null, }, { kind: 'folder', path: 'notes', name: 'notes' }, { kind: 'file', path: 'notes/guide', name: 'guide' }, @@ -57,11 +55,27 @@ describe('buildWorkspaceEntries', () => { path: 'packages/app/src/index.ts', name: 'index.ts', bodyIndexed: false, - assetExt: 'ts', - mediaKind: 'text', }, ]); }); + + test('threads page docExt metadata onto markdown page entries', () => { + const entries = buildWorkspaceEntries( + new Set(['docs/component']), + new Set(), + new Map(), + new Map([ + ['docs/component', { size: 100, modified: '2026-06-24T00:00:00.000Z', docExt: '.mdx' }], + ]), + ); + + expect(entries[0]).toMatchObject({ + kind: 'file', + path: 'docs/component', + name: 'component', + docExt: '.mdx', + }); + }); test('skips a non-markdown file already present in pages', () => { const entries = buildWorkspaceEntries( new Set(['data/example.csv']), @@ -315,15 +329,15 @@ describe('fetchWorkspaceSearchEntries', () => { }); }); - test('maps a kind:file server row to a client kind:file entry', async () => { + test('maps a kind:file server row to a client kind:file name-only entry', async () => { globalThis.fetch = (async () => new Response( JSON.stringify({ results: [ { kind: 'file', - path: 'data/example.csv', - title: 'data/example.csv', + path: 'assets/photo.png', + title: 'assets/photo.png', score: 7, }, ], @@ -336,12 +350,11 @@ describe('fetchWorkspaceSearchEntries', () => { expect(entries).toEqual([ { kind: 'file', - path: 'data/example.csv', - name: 'example.csv', - title: 'data/example.csv', + path: 'assets/photo.png', + name: 'photo.png', + bodyIndexed: false, + title: 'assets/photo.png', score: 7, - assetExt: 'csv', - mediaKind: null, }, ]); }); diff --git a/packages/app/src/components/command-palette-search.ts b/packages/app/src/components/command-palette-search.ts index 272f1434b..38830e5ee 100644 --- a/packages/app/src/components/command-palette-search.ts +++ b/packages/app/src/components/command-palette-search.ts @@ -1,8 +1,6 @@ import { createWorkspaceSearchCorpus, createWorkspaceSearchDocument, - type InlineAssetMediaKind, - mediaKindForSidebarAssetExtension, searchWorkspaceCorpus, type WorkspaceSearchCorpus, type WorkspaceSearchDocument, @@ -16,22 +14,9 @@ export interface WorkspaceEntry { path: string; name: string; title?: string; + docExt?: string; modifiedTs?: number; bodyIndexed?: boolean; - assetExt?: string; - mediaKind?: InlineAssetMediaKind | null; -} - -function fileIconFields(path: string): { - assetExt?: string; - mediaKind?: InlineAssetMediaKind | null; -} { - const dot = path.lastIndexOf('.'); - const slash = path.lastIndexOf('/'); - if (dot <= slash + 1) return {}; - const assetExt = path.slice(dot + 1).toLowerCase(); - if (assetExt === '') return {}; - return { assetExt, mediaKind: mediaKindForSidebarAssetExtension(assetExt) }; } export interface WorkspaceSearchEntry extends WorkspaceEntry { @@ -85,14 +70,16 @@ export function buildWorkspaceEntries( for (const path of pages) { seenFilePaths.add(path); - const modified = pageMeta.get(path)?.modified; + const meta = pageMeta.get(path); + const modified = meta?.modified; const title = pageTitles.get(path); entries.push({ kind: 'file', path, name: workspaceSearchBasename(path), - ...(title ? { title } : {}), - ...(modified ? { modifiedTs: Date.parse(modified) } : {}), + ...(title && { title }), + ...(meta?.docExt && { docExt: meta.docExt }), + ...(modified && { modifiedTs: Date.parse(modified) }), }); } for (const path of filePaths) { @@ -103,7 +90,6 @@ export function buildWorkspaceEntries( path, name: workspaceSearchBasename(path), bodyIndexed: false, - ...fileIconFields(path), }); } for (const path of folderPaths) { @@ -152,7 +138,7 @@ function workspaceEntriesFingerprint(entries: readonly WorkspaceEntry[]): string return entries .map( (entry) => - `${entry.kind}\u0000${entry.path}\u0000${entry.title ?? ''}\u0000${entry.modifiedTs ?? 0} ${entry.bodyIndexed === false ? '0' : '1'}`, + `${entry.kind}\u0000${entry.path}\u0000${entry.title ?? ''}\u0000${entry.docExt ?? ''}\u0000${entry.modifiedTs ?? 0} ${entry.bodyIndexed === false ? '0' : '1'}`, ) .join('\u0001'); } @@ -246,15 +232,14 @@ function toWorkspaceSearchEntry( } const name = workspaceSearchBasename(row.path); const kind = row.kind === 'folder' ? 'folder' : 'file'; - const iconFields = row.kind === 'file' ? fileIconFields(row.path) : {}; return { kind, path: row.path, name, + ...(row.kind === 'file' && { bodyIndexed: false }), ...(row.title && { title: row.title }), ...(row.snippet && { snippet: row.snippet }), ...(typeof row.score === 'number' && { score: row.score }), - ...iconFields, }; } diff --git a/packages/app/src/components/file-entry-icon.test.tsx b/packages/app/src/components/file-entry-icon.test.tsx new file mode 100644 index 000000000..3f226330d --- /dev/null +++ b/packages/app/src/components/file-entry-icon.test.tsx @@ -0,0 +1,34 @@ +import { describe, expect, test } from 'bun:test'; +import { File, Film, ImageIcon, Volume2 } from 'lucide-react'; +import { lucideIconToSvgString } from '@/editor/registry/lucide-svg'; +import { + fileEntryPathIconToSvgString, + MARKDOWN_FILE_ICON_PATH_D, + MARKDOWN_FILE_ICON_VIEWBOX, +} from './file-entry-icon'; + +describe('fileEntryPathIconToSvgString', () => { + test('uses the media-specific lucide icon for video assets', () => { + expect(fileEntryPathIconToSvgString('clips/demo.mp4')).toBe(lucideIconToSvgString(Film)); + }); + + test('uses the media-specific lucide icon for audio assets', () => { + expect(fileEntryPathIconToSvgString('audio/theme.mp3')).toBe(lucideIconToSvgString(Volume2)); + }); + + test('keeps the image asset icon mapping', () => { + expect(fileEntryPathIconToSvgString('assets/photo.png')).toBe(lucideIconToSvgString(ImageIcon)); + }); + + test('keeps the custom markdown icon mapping', () => { + expect(fileEntryPathIconToSvgString('notes.md')).toBe( + '`, + ); + }); + + test('keeps generic files on the fallback file icon', () => { + expect(fileEntryPathIconToSvgString('data/example.csv')).toBe(lucideIconToSvgString(File)); + }); +}); diff --git a/packages/app/src/components/file-entry-icon.tsx b/packages/app/src/components/file-entry-icon.tsx new file mode 100644 index 000000000..095a7f45d --- /dev/null +++ b/packages/app/src/components/file-entry-icon.tsx @@ -0,0 +1,132 @@ +import { mediaKindForSidebarAssetExtension } from '@inkeep/open-knowledge-core'; +import { File, Film, FolderOpen, ImageIcon, Volume2 } from 'lucide-react'; +import type { ComponentProps } from 'react'; +import { getFileExtension } from '@/components/file-tree-rename-validation'; +import { lucideIconToSvgString } from '@/editor/registry/lucide-svg'; +import { cn } from '@/lib/utils.ts'; + +export const MARKDOWN_FILE_ICON_VIEWBOX = '0 0 32 32'; +export const MARKDOWN_FILE_ICON_PATH_D = + 'M26.7075 10.2925L19.7075 3.2925C19.6146 3.19967 19.5042 3.12605 19.3829 3.07586C19.2615 3.02568 19.1314 2.9999 19 3H7C6.46957 3 5.96086 3.21071 5.58579 3.58579C5.21071 3.96086 5 4.46957 5 5V14C5 14.2652 5.10536 14.5196 5.29289 14.7071C5.48043 14.8946 5.73478 15 6 15C6.26522 15 6.51957 14.8946 6.70711 14.7071C6.89464 14.5196 7 14.2652 7 14V5H18V11C18 11.2652 18.1054 11.5196 18.2929 11.7071C18.4804 11.8946 18.7348 12 19 12H25V28C25 28.2652 25.1054 28.5196 25.2929 28.7071C25.4804 28.8946 25.7348 29 26 29C26.2652 29 26.5196 28.8946 26.7071 28.7071C26.8946 28.5196 27 28.2652 27 28V11C27.0001 10.8686 26.9743 10.7385 26.9241 10.6172C26.8739 10.4958 26.8003 10.3854 26.7075 10.2925ZM20 6.41375L23.5863 10H20V6.41375ZM18 18H16C15.7348 18 15.4804 18.1054 15.2929 18.2929C15.1054 18.4804 15 18.7348 15 19V26C15 26.2652 15.1054 26.5196 15.2929 26.7071C15.4804 26.8946 15.7348 27 16 27H18C19.1935 27 20.3381 26.5259 21.182 25.682C22.0259 24.8381 22.5 23.6935 22.5 22.5C22.5 21.3065 22.0259 20.1619 21.182 19.318C20.3381 18.4741 19.1935 18 18 18ZM18 25H17V20H18C18.663 20 19.2989 20.2634 19.7678 20.7322C20.2366 21.2011 20.5 21.837 20.5 22.5C20.5 23.163 20.2366 23.7989 19.7678 24.2678C19.2989 24.7366 18.663 25 18 25ZM13 19V26C13 26.2652 12.8946 26.5196 12.7071 26.7071C12.5196 26.8946 12.2652 27 12 27C11.7348 27 11.4804 26.8946 11.2929 26.7071C11.1054 26.5196 11 26.2652 11 26V22.1725L9.31875 24.5737C9.22652 24.7053 9.10396 24.8126 8.96144 24.8868C8.81892 24.9609 8.66064 24.9996 8.5 24.9996C8.33936 24.9996 8.18108 24.9609 8.03856 24.8868C7.89604 24.8126 7.77348 24.7053 7.68125 24.5737L6 22.1725V26C6 26.2652 5.89464 26.5196 5.70711 26.7071C5.51957 26.8946 5.26522 27 5 27C4.73478 27 4.48043 26.8946 4.29289 26.7071C4.10536 26.5196 4 26.2652 4 26V19C4.00009 18.7874 4.06791 18.5804 4.19363 18.409C4.31935 18.2376 4.49642 18.1107 4.69915 18.0467C4.90188 17.9828 5.11971 17.9851 5.32104 18.0533C5.52236 18.1216 5.6967 18.2522 5.81875 18.4263L8.5 22.2563L11.1812 18.4263C11.3033 18.2522 11.4776 18.1216 11.679 18.0533C11.8803 17.9851 12.0981 17.9828 12.3008 18.0467C12.5036 18.1107 12.6807 18.2376 12.8064 18.409C12.9321 18.5804 12.9999 18.7874 13 19Z'; + +function isMarkdownExt(ext: string): boolean { + return ext === '.md' || ext === '.mdx'; +} + +function iconSvgForExt(ext: string): string { + switch (mediaKindForSidebarAssetExtension(ext)) { + case 'image': + return lucideIconToSvgString(ImageIcon); + case 'video': + return lucideIconToSvgString(Film); + case 'audio': + return lucideIconToSvgString(Volume2); + default: + return lucideIconToSvgString(File); + } +} + +function MarkdownFileIcon({ className, ...props }: ComponentProps<'svg'>) { + return ( + + ); +} + +function fileEntryIconFieldsForPath(path: string): { + kind: 'file' | 'folder'; + path: string; + bodyIndexed?: boolean; +} { + const ext = getFileExtension(path).toLowerCase(); + if (!ext) return { kind: 'folder', path }; + if (isMarkdownExt(ext)) return { kind: 'file', path }; + return { kind: 'file', path, bodyIndexed: false }; +} + +export function FileEntryPathIcon({ path, className }: { path: string; className?: string }) { + return ; +} + +export function fileEntryPathIconToSvgString(path: string): string { + const { kind, bodyIndexed } = fileEntryIconFieldsForPath(path); + if (kind === 'folder') return lucideIconToSvgString(FolderOpen); + + const ext = bodyIndexed === false ? getFileExtension(path) : getFileExtension(path) || '.md'; + const normalizedExt = ext.toLowerCase(); + if (isMarkdownExt(normalizedExt)) { + return ( + '` + ); + } + return iconSvgForExt(normalizedExt); +} + +export function FileEntryIcon({ + kind, + path, + docExt, + bodyIndexed, + className, +}: { + kind: 'file' | 'folder'; + path: string; + docExt?: string; + bodyIndexed?: boolean; + className?: string; +}) { + if (kind === 'folder') { + return ( +