Show image previews in optimistic user messages

This commit is contained in:
Mohamed Boudra
2026-02-16 12:19:38 +07:00
parent 75dc802a3a
commit f8f4c55976
10 changed files with 294 additions and 20 deletions
@@ -198,6 +198,7 @@ export function AgentInputArea({
id: messageId,
text,
timestamp: new Date(),
...(images && images.length > 0 ? { images } : {}),
};
// Append to head if streaming (keeps the user message with the current
@@ -314,6 +314,7 @@ export function AgentStreamView({
return (
<UserMessage
message={item.text}
images={item.images}
timestamp={item.timestamp.getTime()}
isFirstInGroup={isFirstInGroup}
isLastInGroup={isLastInGroup}
+63 -16
View File
@@ -1,6 +1,7 @@
import {
View,
Text,
Image,
Pressable,
ActivityIndicator,
type LayoutChangeEvent,
@@ -62,7 +63,7 @@ import {
} from "@/styles/markdown-styles";
import { Colors, Fonts } from "@/constants/theme";
import * as Clipboard from "expo-clipboard";
import type { TodoEntry } from "@/types/stream";
import type { TodoEntry, UserMessageImageAttachment } from "@/types/stream";
import type { ToolCallDetail } from "@server/server/agent/agent-sdk-types";
import {
buildToolCallDisplayModel,
@@ -77,6 +78,7 @@ import { ToolCallDetailsContent } from "./tool-call-details";
interface UserMessageProps {
message: string;
images?: UserMessageImageAttachment[];
timestamp: number;
isFirstInGroup?: boolean;
isLastInGroup?: boolean;
@@ -177,6 +179,24 @@ const userMessageStylesheet = StyleSheet.create((theme) => ({
lineHeight: 22,
overflowWrap: "anywhere",
},
imagePreviewContainer: {
flexDirection: "row",
gap: theme.spacing[2],
flexWrap: "wrap",
},
imagePreviewSpacing: {
marginBottom: theme.spacing[2],
},
imagePill: {
borderRadius: theme.borderRadius.md,
borderWidth: 1,
borderColor: theme.colors.borderAccent,
overflow: "hidden",
},
imageThumbnail: {
width: 48,
height: 48,
},
copyButton: {
alignSelf: "flex-end",
padding: theme.spacing[1],
@@ -192,6 +212,7 @@ const userMessageStylesheet = StyleSheet.create((theme) => ({
export const UserMessage = memo(function UserMessage({
message,
images = [],
timestamp,
isFirstInGroup = true,
isLastInGroup = true,
@@ -201,8 +222,10 @@ export const UserMessage = memo(function UserMessage({
const [copyButtonHovered, setCopyButtonHovered] = useState(false);
const resolvedDisableOuterSpacing =
useDisableOuterSpacing(disableOuterSpacing);
const hasText = message.trim().length > 0;
const hasImages = images.length > 0;
const showCopyButton =
Platform.OS !== "web" || messageHovered || copyButtonHovered;
hasText && (Platform.OS !== "web" || messageHovered || copyButtonHovered);
return (
<View
@@ -227,21 +250,45 @@ export const UserMessage = memo(function UserMessage({
}
>
<View style={userMessageStylesheet.bubble}>
<Text selectable style={userMessageStylesheet.text}>
{message}
</Text>
{hasImages ? (
<View
style={[
userMessageStylesheet.imagePreviewContainer,
hasText ? userMessageStylesheet.imagePreviewSpacing : undefined,
]}
>
{images.map((image, index) => (
<View
key={`${image.uri}-${index}`}
style={userMessageStylesheet.imagePill}
>
<Image
source={{ uri: image.uri }}
style={userMessageStylesheet.imageThumbnail}
/>
</View>
))}
</View>
) : null}
{hasText ? (
<Text selectable style={userMessageStylesheet.text}>
{message}
</Text>
) : null}
</View>
<TurnCopyButton
getContent={() => message}
containerStyle={[
userMessageStylesheet.copyButton,
showCopyButton
? userMessageStylesheet.copyButtonVisible
: userMessageStylesheet.copyButtonHidden,
]}
accessibilityLabel="Copy message"
onHoverChange={setCopyButtonHovered}
/>
{hasText ? (
<TurnCopyButton
getContent={() => message}
containerStyle={[
userMessageStylesheet.copyButton,
showCopyButton
? userMessageStylesheet.copyButtonVisible
: userMessageStylesheet.copyButtonHidden,
]}
accessibilityLabel="Copy message"
onHoverChange={setCopyButtonHovered}
/>
) : null}
</Pressable>
</View>
);