| ... | @@ -4,6 +4,7 @@ import type { | ... | @@ -4,6 +4,7 @@ import type { |
| 4 | ResolvedKeybindingsConfig, | 4 | ResolvedKeybindingsConfig, |
| 5 | ScopedThreadRef, | 5 | ScopedThreadRef, |
| 6 | } from "@t3tools/contracts"; | 6 | } from "@t3tools/contracts"; |
| | 7 | import { isWorkspaceImagePreviewPath } from "@t3tools/shared/filePreview"; |
| 7 | import { VirtualizedFile, type SelectedLineRange } from "@pierre/diffs"; | 8 | import { VirtualizedFile, type SelectedLineRange } from "@pierre/diffs"; |
| 8 | import { Editor } from "@pierre/diffs/editor"; | 9 | import { Editor } from "@pierre/diffs/editor"; |
| 9 | import { EditorProvider, File, type FileOptions, Virtualizer } from "@pierre/diffs/react"; | 10 | import { EditorProvider, File, type FileOptions, Virtualizer } from "@pierre/diffs/react"; |
| ... | @@ -16,6 +17,7 @@ import * as Schema from "effect/Schema"; | ... | @@ -16,6 +17,7 @@ import * as Schema from "effect/Schema"; |
| 16 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | 17 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 17 | | 18 | |
| 18 | import { isBrowserPreviewFile, openFileInPreview } from "~/browser/openFileInPreview"; | 19 | import { isBrowserPreviewFile, openFileInPreview } from "~/browser/openFileInPreview"; |
| | 20 | import { useAssetUrlState } from "~/assets/assetUrls"; |
| 19 | import ChatMarkdown from "~/components/ChatMarkdown"; | 21 | import ChatMarkdown from "~/components/ChatMarkdown"; |
| 20 | import { OpenInPicker } from "~/components/chat/OpenInPicker"; | 22 | import { OpenInPicker } from "~/components/chat/OpenInPicker"; |
| 21 | import { useClientSettings } from "~/hooks/useSettings"; | 23 | import { useClientSettings } from "~/hooks/useSettings"; |
| ... | @@ -113,6 +115,43 @@ const FILE_LINK_REVEAL_UNSAFE_CSS = ` | ... | @@ -113,6 +115,43 @@ const FILE_LINK_REVEAL_UNSAFE_CSS = ` |
| 113 | `; | 115 | `; |
| 114 | type FilePostRender = NonNullable<FileOptions<unknown>["onPostRender"]>; | 116 | type FilePostRender = NonNullable<FileOptions<unknown>["onPostRender"]>; |
| 115 | | 117 | |
| | 118 | function WorkspaceImagePreview(props: { |
| | 119 | readonly environmentId: EnvironmentId; |
| | 120 | readonly threadRef: ScopedThreadRef; |
| | 121 | readonly absolutePath: string; |
| | 122 | readonly alt: string; |
| | 123 | }) { |
| | 124 | const assetUrl = useAssetUrlState(props.environmentId, { |
| | 125 | _tag: "workspace-file", |
| | 126 | threadId: props.threadRef.threadId, |
| | 127 | path: props.absolutePath, |
| | 128 | }); |
| | 129 | const [failedUrl, setFailedUrl] = useState<string | null>(null); |
| | 130 | |
| | 131 | if (assetUrl._tag === "Failure" || (assetUrl._tag === "Success" && failedUrl === assetUrl.url)) { |
| | 132 | return ( |
| | 133 | <div className="flex min-h-0 flex-1 items-center justify-center px-6 text-center text-xs leading-relaxed text-destructive"> |
| | 134 | Unable to load workspace image. |
| | 135 | </div> |
| | 136 | ); |
| | 137 | } |
| | 138 | |
| | 139 | return assetUrl._tag === "Success" ? ( |
| | 140 | <div className="flex min-h-0 flex-1 items-center justify-center overflow-auto p-4"> |
| | 141 | <img |
| | 142 | className="max-h-full max-w-full object-contain" |
| | 143 | src={assetUrl.url} |
| | 144 | alt={props.alt} |
| | 145 | onError={() => setFailedUrl(assetUrl.url)} |
| | 146 | /> |
| | 147 | </div> |
| | 148 | ) : ( |
| | 149 | <div className="flex min-h-0 flex-1 items-center justify-center text-muted-foreground"> |
| | 150 | <LoaderCircle className="size-5 animate-spin" /> |
| | 151 | </div> |
| | 152 | ); |
| | 153 | } |
| | 154 | |
| 116 | function clampFileLine(contents: string, requestedLine: number): number { | 155 | function clampFileLine(contents: string, requestedLine: number): number { |
| 117 | let lineCount = 1; | 156 | let lineCount = 1; |
| 118 | for (let index = 0; index < contents.length; index += 1) { | 157 | for (let index = 0; index < contents.length; index += 1) { |
| ... | @@ -630,7 +669,8 @@ export default function FilePreviewPanel({ | ... | @@ -630,7 +669,8 @@ export default function FilePreviewPanel({ |
| 630 | const openPreview = useAtomCommand(previewEnvironment.open, { | 669 | const openPreview = useAtomCommand(previewEnvironment.open, { |
| 631 | reportFailure: false, | 670 | reportFailure: false, |
| 632 | }); | 671 | }); |
| 633 | const file = useProjectFileQuery(environmentId, cwd, relativePath); | 672 | const isImage = relativePath !== null && isWorkspaceImagePreviewPath(relativePath); |
| | 673 | const file = useProjectFileQuery(environmentId, cwd, relativePath, !isImage); |
| 634 | const [explorerOpen, setExplorerOpen] = useState(initialExplorerOpen); | 674 | const [explorerOpen, setExplorerOpen] = useState(initialExplorerOpen); |
| 635 | const [markdownView, setMarkdownView] = useState<{ | 675 | const [markdownView, setMarkdownView] = useState<{ |
| 636 | path: string | null; | 676 | path: string | null; |
| ... | @@ -818,7 +858,15 @@ export default function FilePreviewPanel({ | ... | @@ -818,7 +858,15 @@ export default function FilePreviewPanel({ |
| 818 | relativePath ? "flex" : "hidden", | 858 | relativePath ? "flex" : "hidden", |
| 819 | )} | 859 | )} |
| 820 | > | 860 | > |
| 821 | {relativePath && file.error && file.data === null ? ( | 861 | {relativePath && isImage && absolutePath ? ( |
| | 862 | <WorkspaceImagePreview |
| | 863 | key={absolutePath} |
| | 864 | environmentId={environmentId} |
| | 865 | threadRef={threadRef} |
| | 866 | absolutePath={absolutePath} |
| | 867 | alt={relativePath} |
| | 868 | /> |
| | 869 | ) : relativePath && file.error && file.data === null ? ( |
| 822 | <div className="flex min-h-0 flex-1 items-center justify-center px-6 text-center text-xs leading-relaxed text-destructive"> | 870 | <div className="flex min-h-0 flex-1 items-center justify-center px-6 text-center text-xs leading-relaxed text-destructive"> |
| 823 | {file.error} | 871 | {file.error} |
| 824 | </div> | 872 | </div> |