diff --git a/src/renderer/components/FileTree/FileTree.tsx b/src/renderer/components/FileTree/FileTree.tsx
index c14d5a4f709..dc7bf9bcdbe 100644
--- a/src/renderer/components/FileTree/FileTree.tsx
+++ b/src/renderer/components/FileTree/FileTree.tsx
@@ -49,6 +49,8 @@ export function FileTree(props: FileTreeProps) {
searchKeyword = '',
onSearchKeywordChange,
searchPlaceholder,
+ searchToolbar,
+ searchClearLabel,
emptyState
} = props
@@ -114,28 +116,31 @@ export function FileTree(props: FileTreeProps) {
return (
-
-
-
onSearchKeywordChange?.(e.target.value)}
- placeholder={searchPlaceholder}
- className="h-8 pr-7 pl-7 text-sm"
- data-testid="file-tree-search-input"
- />
- {searchKeyword && (
-
- )}
+
+
+
+ onSearchKeywordChange?.(e.target.value)}
+ placeholder={searchPlaceholder}
+ className="h-8 min-w-0 flex-1 pr-7 pl-7 text-sm"
+ data-testid="file-tree-search-input"
+ />
+ {searchKeyword && (
+
+ )}
+
+ {searchToolbar}
{tree}
diff --git a/src/renderer/components/FileTree/FileTreeRow.tsx b/src/renderer/components/FileTree/FileTreeRow.tsx
index 318f1d7c3a8..e821aae1112 100644
--- a/src/renderer/components/FileTree/FileTreeRow.tsx
+++ b/src/renderer/components/FileTree/FileTreeRow.tsx
@@ -79,7 +79,6 @@ export function FileTreeRow(props: FileTreeRowProps) {
data-node-id={node.id}
data-kind={node.kind}
onClick={handleRowClick}
- onContextMenu={(e) => e.stopPropagation()}
title={node.name}
style={indent}
className={cn(
diff --git a/src/renderer/components/FileTree/__tests__/FileTree.test.tsx b/src/renderer/components/FileTree/__tests__/FileTree.test.tsx
index e1b33b55727..3d43de52a54 100644
--- a/src/renderer/components/FileTree/__tests__/FileTree.test.tsx
+++ b/src/renderer/components/FileTree/__tests__/FileTree.test.tsx
@@ -166,6 +166,27 @@ describe('FileTree - editable form (all callbacks)', () => {
expect(screen.getByText('Menu for Root')).toBeInTheDocument()
})
+
+ it('stops row context-menu events after opening the row menu', () => {
+ const onWrapperContextMenu = vi.fn()
+
+ render(
+
+ [{ type: 'item', id: `menu-${n.id}`, label: `Menu for ${n.name}`, onSelect: () => {} }]}
+ renderList={passthroughRenderList}
+ />
+
+ )
+
+ const rootRow = screen.getByText('Root').closest('[data-node-id="root"]')!
+ fireEvent.contextMenu(rootRow)
+
+ expect(screen.getByText('Menu for Root')).toBeInTheDocument()
+ expect(onWrapperContextMenu).not.toHaveBeenCalled()
+ })
})
describe('FileTree - icon behaviour', () => {
@@ -243,8 +264,15 @@ describe('FileTree - icon behaviour', () => {
describe('FileTree - search box', () => {
it('does not render the search input when showSearch is omitted', () => {
- render(
)
+ render(
+
Tools}
+ renderList={passthroughRenderList}
+ />
+ )
expect(screen.queryByTestId('file-tree-search-input')).toBeNull()
+ expect(screen.queryByTestId('search-toolbar')).toBeNull()
})
it('renders the search input when showSearch is true', () => {
@@ -290,6 +318,30 @@ describe('FileTree - search box', () => {
expect(onSearchKeywordChange).toHaveBeenCalledWith('a')
})
+ it('renders the search toolbar and keeps the clear button usable', async () => {
+ const onSearchKeywordChange = vi.fn()
+ const user = userEvent.setup()
+ render(
+
+ Refresh
+
+ }
+ onSearchKeywordChange={onSearchKeywordChange}
+ renderList={passthroughRenderList}
+ />
+ )
+
+ expect(screen.getByTestId('search-toolbar')).toBeInTheDocument()
+ await user.click(screen.getByLabelText('Clear files search'))
+ expect(onSearchKeywordChange).toHaveBeenCalledWith('')
+ })
+
it('clears the keyword via the clear button', async () => {
const onSearchKeywordChange = vi.fn()
const user = userEvent.setup()
diff --git a/src/renderer/components/FileTree/types.ts b/src/renderer/components/FileTree/types.ts
index 49bee0ef044..5d1a00602d0 100644
--- a/src/renderer/components/FileTree/types.ts
+++ b/src/renderer/components/FileTree/types.ts
@@ -71,6 +71,10 @@ export interface FileTreeProps {
onSearchKeywordChange?: (keyword: string) => void
/** Placeholder for the search input. */
searchPlaceholder?: string
+ /** Optional trailing toolbar rendered on the search row. */
+ searchToolbar?: React.ReactNode
+ /** Accessible label for the search clear button. */
+ searchClearLabel?: string
emptyState?: React.ReactNode
}
diff --git a/src/renderer/components/chat/panes/ArtifactPane.tsx b/src/renderer/components/chat/panes/ArtifactPane.tsx
index be5b098d0b8..6140bda12b2 100644
--- a/src/renderer/components/chat/panes/ArtifactPane.tsx
+++ b/src/renderer/components/chat/panes/ArtifactPane.tsx
@@ -1,35 +1,39 @@
import { Button, Markdown, Tooltip } from '@cherrystudio/ui'
import { cn } from '@cherrystudio/ui/lib/utils'
-import { usePersistCache } from '@data/hooks/useCache'
import { loggerService } from '@logger'
import ImagePreviewPanel from '@renderer/components/ArtifactPreview/image/ImagePreviewPanel'
import type { OfficePreviewPanelProps } from '@renderer/components/ArtifactPreview/office/OfficePreviewPanel'
import { EmptyState, LoadingState } from '@renderer/components/chat/primitives'
import HtmlPreviewFrame from '@renderer/components/CodeBlockView/HtmlPreviewFrame'
import CodeViewer from '@renderer/components/CodeViewer'
-import { FileTree } from '@renderer/components/FileTree'
+import type { CommandContextMenuExtraItem } from '@renderer/components/command'
+import { FileTree, type FileTreeNode } from '@renderer/components/FileTree'
+import { getEditorIcon } from '@renderer/components/icons/EditorIcon'
+import { FinderIcon } from '@renderer/components/icons/SvgIcon'
+import { useExternalApps } from '@renderer/hooks/useExternalApps'
import { type FileSizeState, useFileSize } from '@renderer/hooks/useFileSize'
import { type IsTextState, useIsTextFile } from '@renderer/hooks/useIsTextFile'
-import { useResizeDrag } from '@renderer/hooks/useResizeDrag'
import { getLanguageByFilePath } from '@renderer/utils/codeLanguage'
+import { buildEditorUrl } from '@renderer/utils/editor'
+import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
import { joinPath } from '@renderer/utils/path'
+import { isMac, isWin } from '@renderer/utils/platform'
import type { FilePath } from '@shared/types/file'
import { toFileUrl } from '@shared/utils/file'
-import { AlertCircle, FileText, Folder, FolderOpen, Maximize2, Minimize2, RotateCw, Sparkles } from 'lucide-react'
-import { AnimatePresence, motion } from 'motion/react'
+import { AlertCircle, FileText, FolderOpen, RotateCw, Sparkles, X } from 'lucide-react'
import {
type ComponentType,
- type MouseEvent as ReactMouseEvent,
+ type KeyboardEvent as ReactKeyboardEvent,
type ReactNode,
useCallback,
useEffect,
+ useMemo,
useRef,
useState
} from 'react'
import { useTranslation } from 'react-i18next'
-import { CHAT_SHELL_TRANSITION } from '../shell/paneLayout'
-import { getVerticalSplitterProps } from '../shell/splitterA11y'
+import { type ArtifactPaneFileSelection, WORKSPACE_ROOT_ID } from './artifactPanePath'
import OpenExternalAppButton from './OpenExternalAppButton'
import { type ArtifactFileTreeModel, isSelectableFileNode, useArtifactFileTreeModel } from './useArtifactFileTreeModel'
@@ -40,27 +44,20 @@ export { normalizeArtifactPaneFilePath, resolveArtifactPaneFileSelection } from
const logger = loggerService.withContext('ArtifactPane')
-export const ARTIFACT_PANE_WIDTH = 460
-export const ARTIFACT_FILE_TREE_DEFAULT_WIDTH = 160
-export const ARTIFACT_FILE_TREE_CACHE_KEY = 'ui.chat.artifact_pane.file_tree.width'
-const ARTIFACT_FILE_TREE_MIN_WIDTH = 80
-const ARTIFACT_FILE_TREE_MAX_WIDTH_OFFSET = 140
-
export interface ArtifactPaneProps {
workspacePath?: string
maximized?: boolean
+ previewFileSelection?: ArtifactPaneFileSelection | null
+ onPreviewClose?: () => void
pdfLayoutPending?: boolean
pdfLayoutRefreshKey?: number
selectedFile?: string | null
onSelectedFileChange?: (file: string | null) => void
- fileTreeOpen?: boolean
- onFileTreeOpenChange?: (open: boolean) => void
/** Caller-owned expanded folder ids. The synthetic workspace root is managed internally. */
fileTreeExpandedIds?: ReadonlySet
onFileTreeExpandedIdsChange?: (next: ReadonlySet) => void
fileTreeSearchKeyword?: string
onFileTreeSearchKeywordChange?: (keyword: string) => void
- onToggleMaximized?: () => void
/** Show a search input inside the file tree that filters nodes by name. */
enableFileSearch?: boolean
}
@@ -100,6 +97,23 @@ const isPdfFile = (name: string) => PDF_EXT.has(extOf(name))
export const isOfficeDocumentFile = (name: string) => OFFICE_DOCUMENT_EXT.has(extOf(name))
export const isImageFile = (name: string) => IMAGE_EXT.has(extOf(name))
+function getPreviewFileTitle(filePath: string): string {
+ const segments = filePath
+ .trim()
+ .split(/[/\\]+/)
+ .filter(Boolean)
+ return segments.at(-1) ?? filePath
+}
+
+function getFileTreeNodeTargetPath(workspacePath: string | undefined, node: { id: string }): string | null {
+ if (!workspacePath) return null
+ return node.id === WORKSPACE_ROOT_ID ? workspacePath : joinPath(workspacePath, node.id)
+}
+
+function renderFileManagerIcon(): ReactNode {
+ return isMac ? :
+}
+
type PdfPreviewPanelComponent = ComponentType<{
filePath: string
fileName: string
@@ -130,87 +144,6 @@ const loadOfficePreviewPanel = () => {
return officePreviewPanelPromise
}
-function getArtifactFileTreeWidthBounds(artifactPaneWidth: number) {
- const minWidth = ARTIFACT_FILE_TREE_MIN_WIDTH
- const maxWidth = Math.max(minWidth, Math.round(artifactPaneWidth - ARTIFACT_FILE_TREE_MAX_WIDTH_OFFSET))
- return { minWidth, maxWidth }
-}
-
-function clampArtifactFileTreeWidth(width: number, artifactPaneWidth: number): number {
- const { minWidth, maxWidth } = getArtifactFileTreeWidthBounds(artifactPaneWidth)
- return Math.min(maxWidth, Math.max(minWidth, Math.round(width)))
-}
-
-function useArtifactFileTreeResize() {
- const [storedWidth, setStoredWidth] = usePersistCache(ARTIFACT_FILE_TREE_CACHE_KEY)
- const artifactPaneRef = useRef(null)
- const paneRef = useRef(null)
- const currentArtifactPaneWidthRef = useRef(ARTIFACT_PANE_WIDTH)
- const paneLeftRef = useRef(0)
- const [artifactPaneWidth, setArtifactPaneWidth] = useState(ARTIFACT_PANE_WIDTH)
- const paneWidth = clampArtifactFileTreeWidth(storedWidth ?? ARTIFACT_FILE_TREE_DEFAULT_WIDTH, artifactPaneWidth)
-
- const measureArtifactPaneWidth = useCallback(() => {
- const width = artifactPaneRef.current?.getBoundingClientRect().width
- return width && Number.isFinite(width) ? width : ARTIFACT_PANE_WIDTH
- }, [])
-
- useEffect(() => {
- const updateArtifactPaneWidth = () => setArtifactPaneWidth(measureArtifactPaneWidth())
- updateArtifactPaneWidth()
-
- const element = artifactPaneRef.current
- if (!element || typeof ResizeObserver === 'undefined') return
-
- const observer = new ResizeObserver(updateArtifactPaneWidth)
- observer.observe(element)
- return () => observer.disconnect()
- }, [measureArtifactPaneWidth])
-
- const handleMouseMove = useCallback(
- (moveEvent: MouseEvent) => {
- setStoredWidth(
- clampArtifactFileTreeWidth(moveEvent.clientX - paneLeftRef.current, currentArtifactPaneWidthRef.current)
- )
- },
- [setStoredWidth]
- )
-
- const { isResizing, startResizing: startResizeDrag } = useResizeDrag({ onMove: handleMouseMove })
-
- const startResizing = useCallback(
- (event: ReactMouseEvent) => {
- const currentArtifactPaneWidth = measureArtifactPaneWidth()
- currentArtifactPaneWidthRef.current = currentArtifactPaneWidth
- setArtifactPaneWidth(currentArtifactPaneWidth)
- paneLeftRef.current = paneRef.current?.getBoundingClientRect().left ?? event.clientX - paneWidth
- startResizeDrag(event)
- },
- [measureArtifactPaneWidth, paneWidth, startResizeDrag]
- )
-
- const setPaneWidth = useCallback(
- // Clamp against the live measured width (same value that feeds the splitter's aria-valuemax),
- // not currentArtifactPaneWidthRef — that ref is only written at mouse-drag start, so a keyboard
- // resize without a prior drag would otherwise clamp to a stale bound and undershoot the max.
- (nextWidth: number) => setStoredWidth(clampArtifactFileTreeWidth(nextWidth, artifactPaneWidth)),
- [artifactPaneWidth, setStoredWidth]
- )
-
- const { minWidth, maxWidth } = getArtifactFileTreeWidthBounds(artifactPaneWidth)
-
- return {
- artifactPaneRef,
- isResizing,
- paneRef,
- paneWidth,
- minWidth,
- maxWidth,
- startResizing,
- setPaneWidth
- }
-}
-
export function ArtifactFilePreview({
workspacePath,
filePath,
@@ -491,71 +424,69 @@ export function ArtifactFilePreview({
interface ArtifactPaneViewProps {
workspacePath?: string
maximized?: boolean
+ previewFileSelection?: ArtifactPaneFileSelection | null
+ onPreviewClose?: () => void
pdfLayoutPending?: boolean
pdfLayoutRefreshKey?: number
enableFileSearch?: boolean
- onToggleMaximized?: () => void
/** The lifted directory-tree model. Owning it above the component lets the
* agent right-pane survive the Host↔Overlay maximize remount without
* rebuilding the tree. */
model: ArtifactFileTreeModel
selectedFile: string | null
onSelectedFileChange: (file: string | null) => void
- treeOpen: boolean
- onTreeOpenChange: (open: boolean) => void
searchKeyword: string
onSearchKeywordChange: (keyword: string) => void
}
/**
- * Presentational artifact pane: renders the file tree (from a supplied
- * `model`) and the selected-file preview. Owns only genuinely-local concerns
- * (panel resize, the selected-file content sniff, the refresh token); all
- * tree state is provided.
+ * Presentational artifact pane: renders file tree and selected-file overlay
+ * preview from the supplied model.
*/
export function ArtifactPaneView({
workspacePath,
maximized = false,
+ previewFileSelection = null,
+ onPreviewClose,
pdfLayoutPending = false,
pdfLayoutRefreshKey = 0,
enableFileSearch = false,
- onToggleMaximized,
model,
selectedFile,
onSelectedFileChange,
- treeOpen,
- onTreeOpenChange,
searchKeyword,
onSearchKeywordChange
}: ArtifactPaneViewProps) {
const { t } = useTranslation()
- const {
- artifactPaneRef,
- isResizing: isFileTreeResizing,
- paneRef: fileTreePaneRef,
- paneWidth: fileTreeWidth,
- minWidth: fileTreeMinWidth,
- maxWidth: fileTreeMaxWidth,
- startResizing: startFileTreeResizing,
- setPaneWidth: setFileTreeWidth
- } = useArtifactFileTreeResize()
-
+ const { data: externalApps } = useExternalApps({ enabled: true })
+ const artifactPaneRef = useRef(null)
+ const overlayRef = useRef(null)
const [contentRefreshToken, setContentRefreshToken] = useState(0)
- const previousWorkspacePathRef = useRef(workspacePath)
// Destructure the stable callbacks so effect/callback deps don't have to
// list the whole `model` (a fresh object every render).
const { refresh, reloadExpandedDirectories } = model
- // Drop the content-refresh token when the workspace changes so the preview
- // doesn't carry a stale forced-reload key into a different tree.
- useEffect(() => {
- if (previousWorkspacePathRef.current !== workspacePath) {
- previousWorkspacePathRef.current = workspacePath
- setContentRefreshToken(0)
- }
- }, [workspacePath])
-
const trimmedFileSearch = enableFileSearch ? searchKeyword.trim() : ''
+ const overlaySelection = previewFileSelection
+ ? previewFileSelection
+ : workspacePath && selectedFile
+ ? { workspacePath, filePath: selectedFile }
+ : null
+ const overlayWorkspacePath = overlaySelection?.workspacePath
+ const overlayFilePath = overlaySelection?.filePath
+ const previewWorkspacePath = overlayWorkspacePath ?? workspacePath
+ const previewFilePath = overlayFilePath ?? selectedFile
+ const previewKey = `${previewWorkspacePath ?? ''}\0${previewFilePath ?? ''}`
+ const previousPreviewKeyRef = useRef(previewKey)
+ const availableEditors = useMemo(
+ () => externalApps?.filter((app) => app.tags.includes('code-editor')) ?? [],
+ [externalApps]
+ )
+ const fileManagerName = useMemo(() => {
+ if (isMac) return t('agent.session.file_manager.finder')
+ if (isWin) return t('agent.session.file_manager.file_explorer')
+ return t('agent.session.file_manager.files')
+ }, [t])
const handleSelectedChange = useCallback(
(id: string | null) => {
@@ -568,74 +499,265 @@ export function ArtifactPaneView({
[model.nodeById, onSelectedFileChange]
)
- const isPdfSelection = selectedFile ? isPdfFile(selectedFile) : false
- const isOfficeDocumentSelection = selectedFile ? isOfficeDocumentFile(selectedFile) : false
- const isImageSelection = selectedFile ? isImageFile(selectedFile) : false
+ const isPdfSelection = previewFilePath ? isPdfFile(previewFilePath) : false
+ const isOfficeDocumentSelection = previewFilePath ? isOfficeDocumentFile(previewFilePath) : false
+ const isImageSelection = previewFilePath ? isImageFile(previewFilePath) : false
const shouldSniffSelectedFile = !isPdfSelection && !isOfficeDocumentSelection && !isImageSelection
- const sniffedIsText = useIsTextFile(workspacePath, selectedFile, { enabled: shouldSniffSelectedFile })
+ const sniffedIsText = useIsTextFile(previewWorkspacePath, previewFilePath, { enabled: shouldSniffSelectedFile })
const isText = shouldSniffSelectedFile ? sniffedIsText : 'binary'
- const fileSize = useFileSize(workspacePath, selectedFile)
+ const fileSize = useFileSize(previewWorkspacePath, previewFilePath)
+
+ useEffect(() => {
+ if (previousPreviewKeyRef.current === previewKey) return
+ previousPreviewKeyRef.current = previewKey
+ setContentRefreshToken(0)
+ }, [previewKey])
+
+ useEffect(() => {
+ if (!overlayWorkspacePath || !overlayFilePath) return
+ overlayRef.current?.focus()
+ }, [overlayFilePath, overlayWorkspacePath])
const handleRefresh = useCallback(() => {
refresh()
- if (treeOpen) {
- reloadExpandedDirectories()
- }
- if (workspacePath && selectedFile && (isText === 'text' || isOfficeDocumentSelection || isImageSelection)) {
- setContentRefreshToken((v) => v + 1)
+ reloadExpandedDirectories()
+ if (
+ overlayWorkspacePath &&
+ overlayFilePath &&
+ (isText === 'text' || isOfficeDocumentSelection || isImageSelection)
+ ) {
+ setContentRefreshToken((value) => value + 1)
}
}, [
- refresh,
- reloadExpandedDirectories,
- selectedFile,
- treeOpen,
- workspacePath,
- isText,
+ isImageSelection,
isOfficeDocumentSelection,
- isImageSelection
+ isText,
+ overlayFilePath,
+ overlayWorkspacePath,
+ refresh,
+ reloadExpandedDirectories
])
- const isSelectedHtmlPreview = selectedFile ? isHtmlFile(selectedFile) : false
+ const handleClosePreview = useCallback(() => {
+ onPreviewClose?.()
+ onSelectedFileChange(null)
+ }, [onPreviewClose, onSelectedFileChange])
+
+ const handleOverlayKeyDown = useCallback(
+ (event: ReactKeyboardEvent) => {
+ if (event.key !== 'Escape') return
+ event.stopPropagation()
+ handleClosePreview()
+ },
+ [handleClosePreview]
+ )
+
+ const openPath = useCallback(
+ async (path: string) => {
+ try {
+ await window.api.file.openPath(path)
+ } catch (error) {
+ window.toast.error(formatErrorMessageWithPrefix(error, t('files.error.open_path', { path })))
+ }
+ },
+ [t]
+ )
+
+ const showInFolder = useCallback(
+ async (path: string) => {
+ try {
+ await window.api.file.showInFolder(path)
+ } catch (error) {
+ window.toast.error(formatErrorMessageWithPrefix(error, t('files.error.open_path', { path })))
+ }
+ },
+ [t]
+ )
+
+ const getFileTreeMenuItems = useCallback(
+ (node: FileTreeNode): readonly CommandContextMenuExtraItem[] => {
+ const targetPath = getFileTreeNodeTargetPath(workspacePath, node)
+ if (!targetPath) return []
+
+ if (node.kind === 'file') {
+ return [
+ {
+ type: 'item',
+ id: 'open-default-app',
+ label: t('agent.preview_pane.default_app'),
+ icon: ,
+ onSelect: () => void openPath(targetPath)
+ },
+ {
+ type: 'item',
+ id: 'show-in-folder',
+ label: fileManagerName,
+ icon: renderFileManagerIcon(),
+ onSelect: () => void showInFolder(targetPath)
+ },
+ ...availableEditors.map((app) => ({
+ type: 'item',
+ id: `open-editor-${app.id}`,
+ label: app.name,
+ icon: getEditorIcon(app),
+ onSelect: () => window.open(buildEditorUrl(app, targetPath))
+ }))
+ ]
+ }
+
+ return [
+ {
+ type: 'item',
+ id: 'open-file-manager',
+ label: fileManagerName,
+ icon: renderFileManagerIcon(),
+ onSelect: () => void openPath(targetPath)
+ },
+ ...availableEditors.map((app) => ({
+ type: 'item',
+ id: `open-editor-${app.id}`,
+ label: app.name,
+ icon: getEditorIcon(app),
+ onSelect: () => window.open(buildEditorUrl(app, targetPath))
+ }))
+ ]
+ },
+ [availableEditors, fileManagerName, openPath, showInFolder, t, workspacePath]
+ )
+
+ const searchToolbar = (
+
+
+
+
+ {workspacePath && }
+
+ )
+
+ const isSelectedHtmlPreview = previewFilePath ? isHtmlFile(previewFilePath) : false
const isSelectedPdfPreview = isPdfSelection
const isSelectedOfficePreview = isOfficeDocumentSelection
const isSelectedImagePreview = isImageSelection
- const openableFilePath = isOfficeDocumentSelection ? selectedFile : null
- const maximizeLabel = t(maximized ? 'agent.preview_pane.minimize' : 'agent.preview_pane.maximize')
- const FileTreeIcon = treeOpen ? FolderOpen : Folder
- const MaximizeIcon = maximized ? Minimize2 : Maximize2
+ const renderOverlay = () => {
+ if (!overlaySelection) return null
- const renderRight = () => {
- if (!workspacePath) {
- return (
+ return (
+
+
+
+ {getPreviewFileTitle(overlaySelection.filePath)}
+
+
+
+
+
+
+
+ ) : undefined
+ }
+ />
+
+
+ )
+ }
+
+ const renderFileTree = () =>
+ model.isLoading ? (
+
+ ) : (
+
+ {model.error
+ ? t('common.error')
+ : trimmedFileSearch
+ ? t('agent.preview_pane.no_search_results')
+ : workspacePath
+ ? t('agent.preview_pane.empty.title')
+ : t('agent.preview_pane.empty.description')}
+
+ }
+ />
+ )
+
+ if (!workspacePath && !overlaySelection) {
+ return (
+