feat(app): store assistant image previews as assets
This commit is contained in:
@@ -25,6 +25,10 @@ import { lineNumberGutterWidth } from "@/components/code-insets";
|
||||
import { isRenderedMarkdownFile } from "@/components/file-pane-render-mode";
|
||||
import { isWeb } from "@/constants/platform";
|
||||
import { createMarkdownStyles } from "@/styles/markdown-styles";
|
||||
import type { AttachmentMetadata } from "@/attachments/types";
|
||||
import { useAttachmentPreviewUrl } from "@/attachments/use-attachment-preview-url";
|
||||
import { persistAttachmentFromBase64 } from "@/attachments/service";
|
||||
import { createPreviewAttachmentId, getFileNameFromPath } from "@/attachments/utils";
|
||||
|
||||
interface CodeLineProps {
|
||||
tokens: HighlightToken[];
|
||||
@@ -40,6 +44,7 @@ interface FilePreviewBodyProps {
|
||||
showDesktopWebScrollbar: boolean;
|
||||
isMobile: boolean;
|
||||
filePath: string;
|
||||
imagePreviewUri: string | null;
|
||||
}
|
||||
|
||||
function trimNonEmpty(value: string | null | undefined): string | null {
|
||||
@@ -60,6 +65,32 @@ function formatFileSize({ size }: { size: number }): string {
|
||||
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
async function createFilePanePreview(file: ExplorerFile | null): Promise<{
|
||||
file: ExplorerFile | null;
|
||||
imageAttachment: AttachmentMetadata | null;
|
||||
}> {
|
||||
if (!file || file.kind !== "image" || !file.content) {
|
||||
return { file, imageAttachment: null };
|
||||
}
|
||||
|
||||
const { content: _content, ...imageFile } = file;
|
||||
const imageAttachment = await persistAttachmentFromBase64({
|
||||
id: createPreviewAttachmentId({
|
||||
base64: file.content,
|
||||
mimeType: file.mimeType ?? "image/png",
|
||||
path: file.path,
|
||||
}),
|
||||
base64: file.content,
|
||||
mimeType: file.mimeType,
|
||||
fileName: getFileNameFromPath(file.path),
|
||||
});
|
||||
|
||||
return {
|
||||
file: imageFile,
|
||||
imageAttachment,
|
||||
};
|
||||
}
|
||||
|
||||
const CodeLine = React.memo(function CodeLine({
|
||||
tokens,
|
||||
lineNumber,
|
||||
@@ -116,6 +147,7 @@ function FilePreviewBody({
|
||||
showDesktopWebScrollbar,
|
||||
isMobile,
|
||||
filePath,
|
||||
imagePreviewUri,
|
||||
}: FilePreviewBodyProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const isDark = theme.colorScheme === "dark";
|
||||
@@ -230,7 +262,16 @@ function FilePreviewBody({
|
||||
);
|
||||
}
|
||||
|
||||
if (preview.kind === "image" && preview.content) {
|
||||
if (preview.kind === "image") {
|
||||
if (!imagePreviewUri) {
|
||||
return (
|
||||
<View style={styles.centerState}>
|
||||
<ActivityIndicator size="small" />
|
||||
<Text style={styles.loadingText}>Loading file…</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.previewScrollContainer}>
|
||||
<RNScrollView
|
||||
@@ -245,7 +286,7 @@ function FilePreviewBody({
|
||||
>
|
||||
<RNImage
|
||||
source={{
|
||||
uri: `data:${preview.mimeType ?? "image/png"};base64,${preview.content}`,
|
||||
uri: imagePreviewUri,
|
||||
}}
|
||||
style={styles.previewImage}
|
||||
resizeMode="contain"
|
||||
@@ -292,11 +333,17 @@ export function FilePane({
|
||||
normalizedFilePath,
|
||||
"file",
|
||||
);
|
||||
return { file: payload.file ?? null, error: payload.error ?? null };
|
||||
const preview = await createFilePanePreview(payload.file ?? null);
|
||||
return {
|
||||
file: preview.file,
|
||||
imageAttachment: preview.imageAttachment,
|
||||
error: payload.error ?? null,
|
||||
};
|
||||
},
|
||||
staleTime: 5_000,
|
||||
refetchOnMount: true,
|
||||
});
|
||||
const imagePreviewUri = useAttachmentPreviewUrl(query.data?.imageAttachment ?? null);
|
||||
|
||||
return (
|
||||
<View style={styles.container} testID="workspace-file-pane">
|
||||
@@ -312,6 +359,7 @@ export function FilePane({
|
||||
showDesktopWebScrollbar={showDesktopWebScrollbar}
|
||||
isMobile={isMobile}
|
||||
filePath={filePath}
|
||||
imagePreviewUri={imagePreviewUri}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -78,11 +78,17 @@ import {
|
||||
setAssistantImageMetadata,
|
||||
} from "@/utils/assistant-image-metadata";
|
||||
import { resolveAssistantImageSource } from "@/utils/assistant-image-source";
|
||||
import {
|
||||
createPreviewAttachmentId,
|
||||
getFileNameFromPath,
|
||||
parseImageDataUrl,
|
||||
} from "@/attachments/utils";
|
||||
export type { InlinePathTarget } from "@/utils/inline-path";
|
||||
import { PlanCard } from "./plan-card";
|
||||
import { useToolCallSheet } from "./tool-call-sheet";
|
||||
import { ToolCallDetailsContent } from "./tool-call-details";
|
||||
import { useAttachmentPreviewUrl } from "@/attachments/use-attachment-preview-url";
|
||||
import { persistAttachmentFromBase64, persistAttachmentFromDataUrl } from "@/attachments/service";
|
||||
import type { DaemonClient } from "@server/client/daemon-client";
|
||||
import { isWeb, isNative } from "@/constants/platform";
|
||||
|
||||
@@ -595,6 +601,7 @@ function AssistantMarkdownImage({
|
||||
() => resolveAssistantImageSource({ source, workspaceRoot }),
|
||||
[source, workspaceRoot],
|
||||
);
|
||||
const dataImage = useMemo(() => parseImageDataUrl(source), [source]);
|
||||
const containerStyle = useMemo<StyleProp<ViewStyle>>(
|
||||
() => ({
|
||||
marginTop: hasLeadingContent ? theme.spacing[4] : 0,
|
||||
@@ -621,16 +628,44 @@ function AssistantMarkdownImage({
|
||||
if (payload.error) {
|
||||
throw new Error(payload.error);
|
||||
}
|
||||
if (!payload.file || payload.file.kind !== "image" || !payload.file.content) {
|
||||
const file = payload.file;
|
||||
if (!file || file.kind !== "image" || !file.content) {
|
||||
throw new Error("Image preview unavailable.");
|
||||
}
|
||||
|
||||
return `data:${payload.file.mimeType ?? "image/png"};base64,${payload.file.content}`;
|
||||
return await persistAttachmentFromBase64({
|
||||
id: createPreviewAttachmentId({
|
||||
base64: file.content,
|
||||
mimeType: file.mimeType ?? "image/png",
|
||||
path: file.path || resolution.path,
|
||||
}),
|
||||
base64: file.content,
|
||||
mimeType: file.mimeType,
|
||||
fileName: getFileNameFromPath(file.path || resolution.path),
|
||||
});
|
||||
},
|
||||
});
|
||||
const dataImageQuery = useQuery({
|
||||
queryKey: ["assistantMarkdownDataImage", dataImage?.cacheKey ?? null],
|
||||
enabled: dataImage !== null,
|
||||
staleTime: 30_000,
|
||||
queryFn: async () => {
|
||||
if (!dataImage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await persistAttachmentFromDataUrl({
|
||||
id: createPreviewAttachmentId(dataImage),
|
||||
dataUrl: source,
|
||||
mimeType: dataImage.mimeType,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const directUri = resolution?.kind === "direct" ? resolution.uri : null;
|
||||
const resolvedUri = directUri ?? query.data ?? null;
|
||||
const fileAssetUri = useAttachmentPreviewUrl(query.data);
|
||||
const dataImageAssetUri = useAttachmentPreviewUrl(dataImageQuery.data);
|
||||
const directUri = resolution?.kind === "direct" && !dataImage ? resolution.uri : null;
|
||||
const resolvedUri = directUri ?? dataImageAssetUri ?? fileAssetUri ?? null;
|
||||
|
||||
if (resolvedUri) {
|
||||
return (
|
||||
@@ -645,7 +680,7 @@ function AssistantMarkdownImage({
|
||||
);
|
||||
}
|
||||
|
||||
if (query.isLoading) {
|
||||
if (query.isLoading || dataImageQuery.isLoading) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
@@ -668,7 +703,11 @@ function AssistantMarkdownImage({
|
||||
]}
|
||||
>
|
||||
<Text style={assistantMessageStylesheet.imageErrorText}>
|
||||
{query.error instanceof Error ? query.error.message : "Unable to load image preview."}
|
||||
{query.error instanceof Error
|
||||
? query.error.message
|
||||
: dataImageQuery.error instanceof Error
|
||||
? dataImageQuery.error.message
|
||||
: "Unable to load image preview."}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user