import type { EditorId, EnvironmentId, ResolvedKeybindingsConfig, ScopedThreadRef, } from "@t3tools/contracts"; import { isWorkspaceImagePreviewPath } from "@t3tools/shared/filePreview"; import { VirtualizedFile, type SelectedLineRange } from "@pierre/diffs"; import { Editor } from "@pierre/diffs/editor"; import { EditProvider, File, type FileOptions, Virtualizer } from "@pierre/diffs/react"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { ChevronRight, Code2, Eye, FolderTree, Globe2, LoaderCircle } from "lucide-react"; import * as Schema from "effect/Schema"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { isBrowserPreviewFile, openFileInPreview } from "~/browser/openFileInPreview"; import { useAssetUrlState } from "~/assets/assetUrls"; import ChatMarkdown from "~/components/ChatMarkdown"; import { OpenInPicker } from "~/components/chat/OpenInPicker"; import { useRemoteOpenState } from "~/remoteOpen"; import { useClientSettings } from "~/hooks/useSettings"; import { useTheme } from "~/hooks/useTheme"; import { getLocalStorageItem, setLocalStorageItem, useLocalStorage } from "~/hooks/useLocalStorage"; import { DIFF_SURFACE_THEME_UNSAFE_CSS, resolveDiffThemeName } from "~/lib/diffRendering"; import { cn } from "~/lib/utils"; import { isPreviewSupportedInRuntime } from "~/previewStateStore"; import { resolvePathLinkTarget } from "~/terminal-links"; import { ScrollArea } from "~/components/ui/scroll-area"; import { Toggle } from "~/components/ui/toggle"; import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip"; import { stackedThreadToast, toastManager } from "~/components/ui/toast"; import { type DraftId, useComposerDraftStore } from "~/composerDraftStore"; import { buildFileReviewComment } from "~/reviewCommentContext"; import { assetEnvironment } from "~/state/assets"; import { useEnvironmentHttpBaseUrl, usePrimaryEnvironmentId } from "~/state/environments"; import { previewEnvironment } from "~/state/preview"; import { projectEnvironment } from "~/state/projects"; import { useAtomCommand } from "~/state/use-atom-command"; import { useAtomQueryRunner } from "~/state/use-atom-query-runner"; import FileBrowserPanel from "./FileBrowserPanel"; import { type FileCommentAnnotationEntry, type FileCommentAnnotationGroup, type FileCommentLineAnnotation, formatFileCommentRange, nextFileCommentId, normalizeFileCommentRange, remapFileCommentAnnotations, } from "./fileCommentAnnotations"; import { installFileEditorDismissal } from "./fileEditorDismissal"; import { resolveCenteredFileLineScrollTop } from "./fileLineReveal"; import { DiffCommentAnnotation } from "../diffs/DiffCommentAnnotation"; import { projectFileCacheKey, projectFileEditorCacheKey } from "./fileContentRevision"; import { fileBreadcrumbs } from "./filePath"; import { isMarkdownPreviewFile, setMarkdownTaskChecked } from "./filePreviewMode"; import { FileSaveCoordinator } from "./fileSaveCoordinator"; import { confirmProjectFileQueryData, getOptimisticProjectFileQueryData, setProjectFileQueryData, useProjectFileQuery, } from "./projectFilesQueryState"; interface FilePreviewPanelProps { environmentId: EnvironmentId; cwd: string; projectName: string; relativePath: string | null; threadRef: ScopedThreadRef; composerDraftTarget: ScopedThreadRef | DraftId; keybindings: ResolvedKeybindingsConfig; availableEditors: ReadonlyArray; revealLine: number | null; revealRequestId: number; onOpenFile: (relativePath: string) => void; onPendingChange: (relativePath: string, pending: boolean) => void; } const FILE_EXPLORER_STORAGE_KEY = "t3code.fileExplorerOpen"; const RENDER_MARKDOWN_STORAGE_KEY = "t3code.renderMarkdown"; const FILE_SAVE_DEBOUNCE_MS = 500; const FILE_LINK_REVEAL_ATTRIBUTE = "data-file-link-reveal"; const FILE_LINK_REVEAL_UNSAFE_CSS = ` ${DIFF_SURFACE_THEME_UNSAFE_CSS} diffs-container { --diffs-bg: var(--code-background, var(--background)) !important; --diffs-light-bg: var(--code-background, var(--background)) !important; --diffs-dark-bg: var(--code-background, var(--background)) !important; background-color: var(--code-background, var(--background)) !important; color: var(--code-foreground, var(--foreground)) !important; } [${FILE_LINK_REVEAL_ATTRIBUTE}][data-line] { background-color: light-dark( color-mix( in lab, var(--diffs-computed-diff-line-bg) 82%, var(--diffs-bg-selection-override, var(--diffs-selection-base)) ), color-mix( in lab, var(--diffs-computed-diff-line-bg) 75%, var(--diffs-bg-selection-override, var(--diffs-selection-base)) ) ) !important; } [${FILE_LINK_REVEAL_ATTRIBUTE}][data-column-number] { background-color: light-dark( color-mix( in lab, var(--diffs-computed-diff-line-bg) 75%, var(--diffs-bg-selection-number-override, var(--diffs-selection-base)) ), color-mix( in lab, var(--diffs-computed-diff-line-bg) 60%, var(--diffs-bg-selection-number-override, var(--diffs-selection-base)) ) ) !important; color: var(--diffs-selection-number-fg) !important; } `; type FilePostRender = NonNullable["onPostRender"]>; function WorkspaceImagePreview(props: { readonly environmentId: EnvironmentId; readonly threadRef: ScopedThreadRef; readonly absolutePath: string; readonly alt: string; }) { const assetUrl = useAssetUrlState(props.environmentId, { _tag: "workspace-file", threadId: props.threadRef.threadId, path: props.absolutePath, }); const [failedUrl, setFailedUrl] = useState(null); if (assetUrl._tag === "Failure" || (assetUrl._tag === "Success" && failedUrl === assetUrl.url)) { return (
Unable to load workspace image.
); } return assetUrl._tag === "Success" ? (
{props.alt} setFailedUrl(assetUrl.url)} />
) : (
); } function clampFileLine(contents: string, requestedLine: number): number { let lineCount = 1; for (let index = 0; index < contents.length; index += 1) { const character = contents.charCodeAt(index); if (character === 10) { lineCount += 1; } else if (character === 13) { lineCount += 1; if (contents.charCodeAt(index + 1) === 10) index += 1; } } return Math.min(Math.max(1, requestedLine), lineCount); } function updateFileLinkReveal(fileContainer: HTMLElement, line: number | null): void { const root = fileContainer.shadowRoot ?? fileContainer; for (const element of root.querySelectorAll(`[${FILE_LINK_REVEAL_ATTRIBUTE}]`)) { element.removeAttribute(FILE_LINK_REVEAL_ATTRIBUTE); } if (line === null) return; root .querySelector(`[data-line="${line}"]`) ?.setAttribute(FILE_LINK_REVEAL_ATTRIBUTE, ""); root .querySelector(`[data-column-number="${line}"]`) ?.setAttribute(FILE_LINK_REVEAL_ATTRIBUTE, ""); } /** * Frames to keep retrying while the file contents or line metrics are not * available yet (fresh mounts hydrate asynchronously). */ const REVEAL_MAX_ATTEMPTS = 30; /** * After scrolling to the target, hold it for a short window so late * programmatic scroll resets (editable-editor focus and state restoration) * cannot silently snap the file back to the top. Real user input cancels the * guard immediately. */ const REVEAL_GUARD_FRAMES = 20; const REVEAL_GUARD_TOLERANCE_PX = 2; interface FileRevealState { frameId: number | null; cancelGuard: (() => void) | null; handledRequestId: number | null; latestRequestId: number | null; } function useFileLineReveal( relativePath: string | null, revealLine: number | null, revealRequestId: number, ): FilePostRender { const [revealStatesByPath] = useState(() => new Map()); return useCallback( (fileContainer, instance, phase) => { if (relativePath === null) return; const existingState = revealStatesByPath.get(relativePath); const state: FileRevealState = existingState ?? { frameId: null, cancelGuard: null, handledRequestId: null, latestRequestId: null, }; if (!existingState) revealStatesByPath.set(relativePath, state); const cancelPendingReveal = () => { if (state.frameId !== null) { cancelAnimationFrame(state.frameId); state.frameId = null; } state.cancelGuard?.(); }; if (phase === "unmount") { cancelPendingReveal(); return; } const contents = instance.file?.contents; const targetLine = revealLine === null || contents === undefined ? null : clampFileLine(contents, revealLine); updateFileLinkReveal(fileContainer, targetLine); if (!(instance instanceof VirtualizedFile)) return; if (state.latestRequestId !== revealRequestId) { cancelPendingReveal(); state.latestRequestId = revealRequestId; state.handledRequestId = null; } if (revealLine === null) { fileContainer.style.minHeight = ""; return; } const scrollContainer = fileContainer.closest(".file-preview-virtualizer"); if (!scrollContainer) return; fileContainer.style.minHeight = `${Math.ceil( Math.max(instance.height, scrollContainer.clientHeight), )}px`; if (state.handledRequestId === revealRequestId || state.frameId !== null) { return; } const resolveScrollTarget = (line: number): number | null => { const linePosition = instance.getLinePosition(line); if (!linePosition) return null; const scrollContainerRect = scrollContainer.getBoundingClientRect(); const fileTop = scrollContainer.scrollTop + fileContainer.getBoundingClientRect().top - scrollContainerRect.top; const root = fileContainer.shadowRoot ?? fileContainer; const renderedLineElement = root.querySelector(`[data-line="${line}"]`); const renderedLineRect = renderedLineElement?.getBoundingClientRect(); return resolveCenteredFileLineScrollTop({ scrollTop: scrollContainer.scrollTop, scrollHeight: scrollContainer.scrollHeight, viewportTop: scrollContainerRect.top, viewportHeight: scrollContainer.clientHeight, fileTop, estimatedLine: linePosition, ...(renderedLineRect && renderedLineRect.height > 0 ? { renderedLine: { top: renderedLineRect.top, height: renderedLineRect.height, }, } : {}), }); }; const guardScrollTarget = (line: number) => { let framesLeft = REVEAL_GUARD_FRAMES; let guardFrameId: number | null = null; const cancelGuard = () => { if (guardFrameId !== null) { cancelAnimationFrame(guardFrameId); guardFrameId = null; } scrollContainer.removeEventListener("wheel", cancelGuard); scrollContainer.removeEventListener("touchstart", cancelGuard); scrollContainer.removeEventListener("pointerdown", cancelGuard, true); window.removeEventListener("keydown", cancelGuard, true); if (state.cancelGuard === cancelGuard) state.cancelGuard = null; }; scrollContainer.addEventListener("wheel", cancelGuard, { passive: true }); scrollContainer.addEventListener("touchstart", cancelGuard, { passive: true }); // Pierre stops gutter pointer events from bubbling. Listen in capture // so starting a comment cancels the reveal guard before the row expands. scrollContainer.addEventListener("pointerdown", cancelGuard, { passive: true, capture: true, }); window.addEventListener("keydown", cancelGuard, true); const holdTarget = () => { guardFrameId = null; framesLeft -= 1; if (framesLeft <= 0 || !scrollContainer.isConnected) { cancelGuard(); return; } const targetTop = resolveScrollTarget(line); if ( targetTop !== null && Math.abs(scrollContainer.scrollTop - targetTop) > REVEAL_GUARD_TOLERANCE_PX ) { scrollContainer.scrollTop = targetTop; } guardFrameId = requestAnimationFrame(holdTarget); }; guardFrameId = requestAnimationFrame(holdTarget); state.cancelGuard = cancelGuard; }; const scheduleReveal = (attempt: number) => { state.frameId = requestAnimationFrame(() => { state.frameId = null; if (state.latestRequestId !== revealRequestId || !fileContainer.isConnected) { return; } // Contents and line metrics can lag the first post-render on fresh // mounts; clamping against missing contents would scroll to line 1 // and wrongly mark the request handled. const currentContents = instance.file?.contents; const line = currentContents === undefined ? null : clampFileLine(currentContents, revealLine); const targetTop = line === null ? null : resolveScrollTarget(line); if (line === null || targetTop === null) { if (attempt < REVEAL_MAX_ATTEMPTS) scheduleReveal(attempt + 1); return; } updateFileLinkReveal(fileContainer, line); scrollContainer.scrollTop = targetTop; state.handledRequestId = revealRequestId; guardScrollTarget(line); }); }; scheduleReveal(0); }, [revealStatesByPath, relativePath, revealLine, revealRequestId], ); } interface EditableFileSurfaceProps { environmentId: EnvironmentId; cwd: string; relativePath: string; composerDraftTarget: ScopedThreadRef | DraftId; contents: string; resolvedTheme: "light" | "dark"; revealRequestId: number; wordWrap: boolean; onPostRender: FilePostRender; onPendingChange: (relativePath: string, pending: boolean) => void; } interface FileSelectionOverride { revealRequestId: number; range: SelectedLineRange | null; } function useFileSaveCoordinator({ environmentId, cwd, relativePath, onPendingChange, }: Pick< EditableFileSurfaceProps, "environmentId" | "cwd" | "relativePath" | "onPendingChange" >): FileSaveCoordinator { const writeFile = useAtomCommand(projectEnvironment.writeFile); const coordinator = useMemo( () => new FileSaveCoordinator({ debounceMs: FILE_SAVE_DEBOUNCE_MS, onPendingChange: (pending) => onPendingChange(relativePath, pending), persist: (nextContents) => writeFile({ environmentId, input: { cwd, relativePath, contents: nextContents }, }), onConfirmed: (confirmedContents) => { confirmProjectFileQueryData(environmentId, cwd, relativePath, confirmedContents); }, }), [cwd, environmentId, onPendingChange, relativePath, writeFile], ); useEffect(() => () => coordinator.dispose(), [coordinator]); return coordinator; } function EditableFileSurface({ environmentId, cwd, relativePath, composerDraftTarget, contents, resolvedTheme, revealRequestId, wordWrap, onPostRender, onPendingChange, }: EditableFileSurfaceProps) { const addReviewComment = useComposerDraftStore((store) => store.addReviewComment); const removeReviewComment = useComposerDraftStore((store) => store.removeReviewComment); const [lineAnnotations, setLineAnnotations] = useState([]); const [selectionOverride, setSelectionOverride] = useState(null); const selectedRange = selectionOverride?.revealRequestId === revealRequestId ? selectionOverride.range : null; const setSelectedRange = useCallback( (range: SelectedLineRange | null) => { setSelectionOverride({ revealRequestId, range }); }, [revealRequestId], ); const surfaceRef = useRef(null); const selectionFrameRef = useRef(null); const saveCoordinator = useFileSaveCoordinator({ environmentId, cwd, relativePath, onPendingChange, }); const editor = useMemo( () => new Editor({ persistState: true, persistStateStorage: "inMemory", onChange: (file, nextLineAnnotations) => { setProjectFileQueryData(environmentId, cwd, relativePath, file.contents); saveCoordinator.change(file.contents); if (nextLineAnnotations) { const remapped = remapFileCommentAnnotations( nextLineAnnotations as FileCommentLineAnnotation[], ); setLineAnnotations(remapped); for (const annotation of remapped) { for (const entry of annotation.metadata.entries) { if (entry.kind !== "comment") continue; addReviewComment( composerDraftTarget, buildFileReviewComment({ id: entry.id, filePath: relativePath, startLine: entry.startLine, endLine: entry.endLine, text: entry.text, contents: file.contents, }), ); } } } }, }), [addReviewComment, composerDraftTarget, cwd, environmentId, relativePath, saveCoordinator], ); useEffect( () => () => { editor.cleanUp(); }, [editor], ); const removeAnnotationEntry = useCallback( (entryId: string) => { setSelectedRange(null); removeReviewComment(composerDraftTarget, entryId); setLineAnnotations((current) => { return current.flatMap((annotation) => { const entries = annotation.metadata.entries.filter((entry) => entry.id !== entryId); return entries.length > 0 ? [{ ...annotation, metadata: { entries } }] : []; }); }); }, [composerDraftTarget, removeReviewComment, setSelectedRange], ); const submitAnnotationEntry = useCallback( (entryId: string, text: string) => { setSelectedRange(null); const entry = lineAnnotations .flatMap((annotation) => annotation.metadata.entries) .find((candidate) => candidate.id === entryId); if (entry) { addReviewComment( composerDraftTarget, buildFileReviewComment({ id: entry.id, filePath: relativePath, startLine: entry.startLine, endLine: entry.endLine, text, contents, }), ); } setLineAnnotations((current) => current.map((annotation) => ({ ...annotation, metadata: { entries: annotation.metadata.entries.map((annotationEntry) => annotationEntry.id === entryId ? { ...annotationEntry, kind: "comment", text } : annotationEntry, ), }, })), ); }, [ addReviewComment, composerDraftTarget, contents, lineAnnotations, relativePath, setSelectedRange, ], ); const beginComment = useCallback((range: SelectedLineRange) => { const { startLine, endLine } = normalizeFileCommentRange(range); const draftEntry: FileCommentAnnotationEntry = { id: nextFileCommentId(), kind: "draft", startLine, endLine, text: "", }; setLineAnnotations((current) => { const withoutDraft = current.flatMap((annotation) => { const entries = annotation.metadata.entries.filter((entry) => entry.kind !== "draft"); return entries.length > 0 ? [{ ...annotation, metadata: { entries } }] : []; }); const existingIndex = withoutDraft.findIndex( (annotation) => annotation.lineNumber === endLine, ); if (existingIndex < 0) { return [ ...withoutDraft, { lineNumber: endLine, metadata: { entries: [draftEntry] }, }, ]; } return withoutDraft.map((annotation, index) => index === existingIndex ? { ...annotation, metadata: { entries: [...annotation.metadata.entries, draftEntry] }, } : annotation, ); }); }, []); const hasOpenCommentForm = lineAnnotations.some((annotation) => annotation.metadata.entries.some((entry) => entry.kind === "draft"), ); useEffect(() => { const root = surfaceRef.current; if (!root) return; return installFileEditorDismissal({ root, editor, isBlocked: () => hasOpenCommentForm, onDismiss: () => setSelectedRange(null), }); }, [editor, hasOpenCommentForm, setSelectedRange]); const handleLineSelectionEnd = useCallback( (range: SelectedLineRange | null) => { setSelectedRange(range); if (range) { beginComment(range); } }, [beginComment, setSelectedRange], ); const handlePostRender = useCallback( (fileContainer, instance, phase) => { onPostRender(fileContainer, instance, phase); if (selectionFrameRef.current !== null) { cancelAnimationFrame(selectionFrameRef.current); selectionFrameRef.current = null; } if (phase === "unmount") return; selectionFrameRef.current = requestAnimationFrame(() => { selectionFrameRef.current = null; if (!fileContainer.isConnected) return; instance.setSelectedLines(selectedRange, { notify: false }); }); }, [onPostRender, selectedRange], ); return (
file={{ name: relativePath, contents, cacheKey: projectFileEditorCacheKey( environmentId, cwd, relativePath, contents, editor.getFile(), ), }} options={{ disableFileHeader: true, enableGutterUtility: !hasOpenCommentForm, enableLineSelection: !hasOpenCommentForm, onGutterUtilityClick: setSelectedRange, onLineSelectionChange: setSelectedRange, onLineSelectionEnd: handleLineSelectionEnd, overflow: wordWrap ? "wrap" : "scroll", theme: resolveDiffThemeName(resolvedTheme), themeType: resolvedTheme, unsafeCSS: FILE_LINK_REVEAL_UNSAFE_CSS, onPostRender: handlePostRender, }} selectedLines={selectedRange} lineAnnotations={lineAnnotations} renderAnnotation={(annotation) => (
{annotation.metadata.entries.map((entry) => ( removeAnnotationEntry(entry.id)} onComment={(text) => submitAnnotationEntry(entry.id, text)} onDelete={() => removeAnnotationEntry(entry.id)} /> ))}
)} className="min-h-full" contentEditable />
); } function RenderedMarkdownSurface({ environmentId, cwd, relativePath, contents, threadRef, onPendingChange, }: Omit< EditableFileSurfaceProps, | "resolvedTheme" | "composerDraftTarget" | "revealLine" | "revealRequestId" | "wordWrap" | "onPostRender" > & { threadRef: ScopedThreadRef; }) { const saveCoordinator = useFileSaveCoordinator({ environmentId, cwd, relativePath, onPendingChange, }); return ( { const currentContents = getOptimisticProjectFileQueryData(environmentId, cwd, relativePath)?.contents ?? contents; const nextContents = setMarkdownTaskChecked(currentContents, markerOffset, checked); if (nextContents === currentContents) return; setProjectFileQueryData(environmentId, cwd, relativePath, nextContents); saveCoordinator.change(nextContents); }} /> ); } function initialExplorerOpen(): boolean { try { return getLocalStorageItem(FILE_EXPLORER_STORAGE_KEY, Schema.Boolean) ?? true; } catch (error) { console.error(error); return true; } } export default function FilePreviewPanel({ environmentId, cwd, projectName, relativePath, threadRef, composerDraftTarget, keybindings, availableEditors, revealLine, revealRequestId, onOpenFile, onPendingChange, }: FilePreviewPanelProps) { const { resolvedTheme } = useTheme(); const wordWrap = useClientSettings((settings) => settings.wordWrap); const primaryEnvironmentId = usePrimaryEnvironmentId(); const remoteOpenState = useRemoteOpenState(environmentId); const environmentHttpBaseUrl = useEnvironmentHttpBaseUrl(environmentId); const createAssetUrl = useAtomQueryRunner(assetEnvironment.createUrl, { reportFailure: false, }); const openPreview = useAtomCommand(previewEnvironment.open, { reportFailure: false, }); const isImage = relativePath !== null && isWorkspaceImagePreviewPath(relativePath); const file = useProjectFileQuery(environmentId, cwd, relativePath, !isImage); const [explorerOpen, setExplorerOpen] = useState(initialExplorerOpen); // Reading markdown rendered is a preference, not a property of one file. Keeping // it on the panel meant a thread switch dropped it and forced source back. const [renderMarkdownPreferred, setRenderMarkdownPreferred] = useLocalStorage( RENDER_MARKDOWN_STORAGE_KEY, false, Schema.Boolean, ); // Paired with the path on purpose: each file surface counts its reveals from // one, so a bare id would let a dismissed reveal on one file swallow the first // reveal on the next. const [handledReveal, setHandledReveal] = useState<{ path: string; requestId: number } | null>( null, ); const breadcrumbRef = useRef(null); const isMarkdown = relativePath ? isMarkdownPreviewFile(relativePath) : false; // A reveal still wins over the preference: the line only exists in the source. const renderMarkdown = isMarkdown && renderMarkdownPreferred && (revealLine === null || (handledReveal?.path === relativePath && handledReveal.requestId === revealRequestId)); const canOpenInBrowser = relativePath !== null && isPreviewSupportedInRuntime() && isBrowserPreviewFile(relativePath); const absolutePath = relativePath ? resolvePathLinkTarget(relativePath, cwd) : null; const breadcrumbs = useMemo( () => (relativePath ? fileBreadcrumbs(projectName, relativePath) : []), [projectName, relativePath], ); const onFilePostRender = useFileLineReveal(relativePath, revealLine, revealRequestId); useEffect(() => { const currentCrumb = breadcrumbRef.current?.querySelector( "[data-current-file-crumb='true']", ); currentCrumb?.scrollIntoView({ block: "nearest", inline: "end" }); }, [relativePath]); const toggleExplorer = () => { setExplorerOpen((current) => { const next = !current; try { setLocalStorageItem(FILE_EXPLORER_STORAGE_KEY, next, Schema.Boolean); } catch (error) { console.error(error); } return next; }); }; const handleOpenInBrowser = useCallback(() => { if (!absolutePath || !environmentHttpBaseUrl) return; void (async () => { const result = await openFileInPreview({ threadRef, filePath: absolutePath, httpBaseUrl: environmentHttpBaseUrl, createAssetUrl, openPreview, }); if (result._tag === "Success" || isAtomCommandInterrupted(result)) { return; } const error = squashAtomCommandFailure(result); toastManager.add( stackedThreadToast({ type: "error", title: "Unable to open file in browser", description: error instanceof Error ? error.message : "An error occurred.", }), ); })(); }, [absolutePath, createAssetUrl, environmentHttpBaseUrl, openPreview, threadRef]); return (
{relativePath ? (
{breadcrumbs.map((crumb, index) => (
{index > 0 ? ( ) : null} } > {crumb.label} {crumb.path || projectName}
))}
{absolutePath && (environmentId === primaryEnvironmentId || remoteOpenState.mode !== "local-exec") ? ( ) : null} {isMarkdown ? ( { setRenderMarkdownPreferred(pressed); setHandledReveal( pressed && relativePath !== null ? { path: relativePath, requestId: revealRequestId } : null, ); }} aria-label={renderMarkdown ? "Show markdown source" : "Show rendered markdown"} variant="ghost" size="sm" > {renderMarkdown ? : } } /> {renderMarkdown ? "Show markdown source" : "Show rendered markdown"} ) : null} {canOpenInBrowser ? ( } /> Open file in preview browser ) : null} } /> {explorerOpen ? "Hide file explorer" : "Show file explorer"}
) : null} {relativePath && file.data?.truncated ? (
Preview limited to the first 1 MB of a {file.data.byteLength.toLocaleString()} byte file.
) : null}
{relativePath && isImage && absolutePath ? ( ) : relativePath && file.error && file.data === null ? (
{file.error}
) : relativePath && file.data === null ? (
) : relativePath && file.data ? ( isMarkdown && renderMarkdown ? ( ) : file.data.truncated ? ( ) : ( ) ) : null}
{explorerOpen || relativePath === null ? ( ) : null}
); }