From b4ae3387ce2ce6a7e345e41a5a13367ccc74651e Mon Sep 17 00:00:00 2001 From: Rhiz3K Date: Wed, 15 Jul 2026 06:44:41 +0000 Subject: [PATCH 1/3] fix(web): preview workspace images in file panel --- .../src/components/files/FilePreviewPanel.tsx | 37 ++++++++++++++++++- .../files/projectFilesQueryState.ts | 10 ++++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/files/FilePreviewPanel.tsx b/apps/web/src/components/files/FilePreviewPanel.tsx index 8c430d5d2ab..9ca0fd550b7 100644 --- a/apps/web/src/components/files/FilePreviewPanel.tsx +++ b/apps/web/src/components/files/FilePreviewPanel.tsx @@ -4,6 +4,7 @@ import type { 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 { EditorProvider, File, type FileOptions, Virtualizer } from "@pierre/diffs/react"; @@ -16,6 +17,7 @@ import * as Schema from "effect/Schema"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { isBrowserPreviewFile, openFileInPreview } from "~/browser/openFileInPreview"; +import { useAssetUrl } from "~/assets/assetUrls"; import ChatMarkdown from "~/components/ChatMarkdown"; import { OpenInPicker } from "~/components/chat/OpenInPicker"; import { useClientSettings } from "~/hooks/useSettings"; @@ -113,6 +115,29 @@ const FILE_LINK_REVEAL_UNSAFE_CSS = ` `; type FilePostRender = NonNullable["onPostRender"]>; +function WorkspaceImagePreview(props: { + readonly environmentId: EnvironmentId; + readonly threadRef: ScopedThreadRef; + readonly absolutePath: string; + readonly alt: string; +}) { + const src = useAssetUrl(props.environmentId, { + _tag: "workspace-file", + threadId: props.threadRef.threadId, + path: props.absolutePath, + }); + + return src ? ( +
+ {props.alt} +
+ ) : ( +
+ +
+ ); +} + function clampFileLine(contents: string, requestedLine: number): number { let lineCount = 1; for (let index = 0; index < contents.length; index += 1) { @@ -630,7 +655,8 @@ export default function FilePreviewPanel({ const openPreview = useAtomCommand(previewEnvironment.open, { reportFailure: false, }); - const file = useProjectFileQuery(environmentId, cwd, relativePath); + const isImage = relativePath !== null && isWorkspaceImagePreviewPath(relativePath); + const file = useProjectFileQuery(environmentId, cwd, relativePath, !isImage); const [explorerOpen, setExplorerOpen] = useState(initialExplorerOpen); const [markdownView, setMarkdownView] = useState<{ path: string | null; @@ -818,7 +844,14 @@ export default function FilePreviewPanel({ relativePath ? "flex" : "hidden", )} > - {relativePath && file.error && file.data === null ? ( + {relativePath && isImage && absolutePath ? ( + + ) : relativePath && file.error && file.data === null ? (
{file.error}
diff --git a/apps/web/src/components/files/projectFilesQueryState.ts b/apps/web/src/components/files/projectFilesQueryState.ts index 191b97d6a96..0d3fb8dd941 100644 --- a/apps/web/src/components/files/projectFilesQueryState.ts +++ b/apps/web/src/components/files/projectFilesQueryState.ts @@ -6,7 +6,7 @@ import type { } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import * as Option from "effect/Option"; -import { AsyncResult } from "effect/unstable/reactivity"; +import { AsyncResult, Atom } from "effect/unstable/reactivity"; import { useCallback } from "react"; import { appAtomRegistry } from "~/rpc/atomRegistry"; @@ -14,6 +14,9 @@ import { projectEnvironment } from "~/state/projects"; import { executeAtomQuery } from "@t3tools/client-runtime/state/runtime"; const EMPTY_PROJECT_FILE_PATH = ""; +const EMPTY_PROJECT_FILE_QUERY_ATOM = Atom.make( + AsyncResult.initial(false), +).pipe(Atom.withLabel("project-file-query:empty")); function optimisticFileAtom(environmentId: EnvironmentId, cwd: string, relativePath: string) { return projectEnvironment.optimisticFile({ environmentId, cwd, relativePath }); } @@ -137,8 +140,11 @@ export function useProjectFileQuery( environmentId: EnvironmentId, cwd: string, relativePath: string | null, + enabled = true, ): ProjectQueryState { - const atom = getProjectFileQueryAtom(environmentId, cwd, relativePath); + const atom = enabled + ? getProjectFileQueryAtom(environmentId, cwd, relativePath) + : EMPTY_PROJECT_FILE_QUERY_ATOM; const result = useAtomValue(atom); const refreshAtom = useAtomRefresh(atom); const refresh = useCallback(() => refreshAtom(), [refreshAtom]); From e93fda64abb4a526509c175135dfd82b523ec05a Mon Sep 17 00:00:00 2001 From: Rhiz3K Date: Wed, 15 Jul 2026 07:17:12 +0000 Subject: [PATCH 2/3] fix(web): surface workspace image load failures --- apps/web/src/assets/assetUrls.ts | 24 +++++++++++++++++-- .../src/components/files/FilePreviewPanel.tsx | 22 +++++++++++++---- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/apps/web/src/assets/assetUrls.ts b/apps/web/src/assets/assetUrls.ts index 673b093e333..701af3a79fc 100644 --- a/apps/web/src/assets/assetUrls.ts +++ b/apps/web/src/assets/assetUrls.ts @@ -9,7 +9,15 @@ import { usePreparedConnection } from "~/state/session"; export { resolveAssetUrl } from "@t3tools/client-runtime/state/assets"; -export function useAssetUrl(environmentId: EnvironmentId, resource: AssetResource): string | null { +export type AssetUrlState = + | { readonly _tag: "Loading" } + | { readonly _tag: "Failure" } + | { readonly _tag: "Success"; readonly url: string }; + +export function useAssetUrlState( + environmentId: EnvironmentId, + resource: AssetResource, +): AssetUrlState { const preparedConnection = usePreparedConnection(environmentId); const result = useAtomValue( assetEnvironment.createUrl({ @@ -17,10 +25,22 @@ export function useAssetUrl(environmentId: EnvironmentId, resource: AssetResourc input: { resource }, }), ); + if (result._tag === "Failure") { + return { _tag: "Failure" }; + } if (preparedConnection._tag === "None" || result._tag !== "Success") { + return { _tag: "Loading" }; + } + const url = resolveAssetUrl(preparedConnection.value.httpBaseUrl, result.value.relativeUrl); + return url === null ? { _tag: "Failure" } : { _tag: "Success", url }; +} + +export function useAssetUrl(environmentId: EnvironmentId, resource: AssetResource): string | null { + const result = useAssetUrlState(environmentId, resource); + if (result._tag !== "Success") { return null; } - return resolveAssetUrl(preparedConnection.value.httpBaseUrl, result.value.relativeUrl); + return result.url; } export function useAssetUrls( diff --git a/apps/web/src/components/files/FilePreviewPanel.tsx b/apps/web/src/components/files/FilePreviewPanel.tsx index 9ca0fd550b7..b0a80fb56e7 100644 --- a/apps/web/src/components/files/FilePreviewPanel.tsx +++ b/apps/web/src/components/files/FilePreviewPanel.tsx @@ -17,7 +17,7 @@ import * as Schema from "effect/Schema"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { isBrowserPreviewFile, openFileInPreview } from "~/browser/openFileInPreview"; -import { useAssetUrl } from "~/assets/assetUrls"; +import { useAssetUrlState } from "~/assets/assetUrls"; import ChatMarkdown from "~/components/ChatMarkdown"; import { OpenInPicker } from "~/components/chat/OpenInPicker"; import { useClientSettings } from "~/hooks/useSettings"; @@ -121,15 +121,29 @@ function WorkspaceImagePreview(props: { readonly absolutePath: string; readonly alt: string; }) { - const src = useAssetUrl(props.environmentId, { + const assetUrl = useAssetUrlState(props.environmentId, { _tag: "workspace-file", threadId: props.threadRef.threadId, path: props.absolutePath, }); + const [failedUrl, setFailedUrl] = useState(null); - return src ? ( + if (assetUrl._tag === "Failure" || (assetUrl._tag === "Success" && failedUrl === assetUrl.url)) { + return ( +
+ Unable to load workspace image. +
+ ); + } + + return assetUrl._tag === "Success" ? (
- {props.alt} + {props.alt} setFailedUrl(assetUrl.url)} + />
) : (
From 1aa1c2ab4e3d379a2ca9314de6f7e0337308a25c Mon Sep 17 00:00:00 2001 From: Rhiz3K Date: Wed, 15 Jul 2026 07:24:26 +0000 Subject: [PATCH 3/3] fix(web): reset image errors when selection changes --- apps/web/src/components/files/FilePreviewPanel.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/components/files/FilePreviewPanel.tsx b/apps/web/src/components/files/FilePreviewPanel.tsx index b0a80fb56e7..6ddd38e9d25 100644 --- a/apps/web/src/components/files/FilePreviewPanel.tsx +++ b/apps/web/src/components/files/FilePreviewPanel.tsx @@ -860,6 +860,7 @@ export default function FilePreviewPanel({ > {relativePath && isImage && absolutePath ? (