app: restore per-turn copy actions and refine message copy UI

This commit is contained in:
Mohamed Boudra
2026-02-06 21:49:09 +07:00
parent 1413c63990
commit f2c88ae073
2 changed files with 61 additions and 85 deletions

View File

@@ -396,14 +396,23 @@ export function AgentStreamView({
}
const gapBelow = getGapBelow(item, index);
const nextItem = flatListData[index - 1];
const isEndOfAssistantTurn =
item.kind === "assistant_message" &&
(nextItem?.kind === "user_message" ||
(nextItem === undefined && agent.status !== "running"));
const getTurnContent = () => collectAssistantTurnContent(flatListData, index);
return (
<View style={[stylesheet.streamItemWrapper, { marginBottom: gapBelow }]}>
{content}
{isEndOfAssistantTurn ? (
<TurnCopyButton getContent={getTurnContent} />
) : null}
</View>
);
},
[getGapBelow, renderStreamItemContent]
[getGapBelow, renderStreamItemContent, flatListData, agent.status]
);
const pendingPermissionItems = useMemo(
@@ -483,18 +492,7 @@ export function AgentStreamView({
}, [agentId, pendingPermissionItems.length, streamHead, streamItems]);
const showWorkingIndicator = agent.status === "running";
const newestItem = flatListData[0] ?? null;
const latestTurnContent = useMemo(() => {
if (flatListData.length === 0) {
return "";
}
return collectAssistantTurnContent(flatListData, 0);
}, [flatListData]);
const showCopyButton =
agent.status !== "running" &&
newestItem?.kind === "assistant_message" &&
latestTurnContent.trim().length > 0;
const showBottomBar = showWorkingIndicator || showCopyButton || isVoiceMode;
const showBottomBar = showWorkingIndicator || isVoiceMode;
const listHeaderComponent = useMemo(() => {
const hasPermissions = pendingPermissionItems.length > 0;
@@ -504,11 +502,7 @@ export function AgentStreamView({
return null;
}
const leftContent = showWorkingIndicator ? (
<WorkingIndicator />
) : showCopyButton ? (
<TurnCopyButton getContent={() => latestTurnContent} />
) : null;
const leftContent = showWorkingIndicator ? <WorkingIndicator /> : null;
return (
<View style={stylesheet.contentWrapper}>
@@ -564,8 +558,6 @@ export function AgentStreamView({
renderStreamItemContent,
tightGap,
showBottomBar,
showCopyButton,
latestTurnContent,
isVoiceMode,
]);
@@ -574,14 +566,12 @@ export function AgentStreamView({
pendingPermissionCount: pendingPermissionItems.length,
showWorkingIndicator,
showBottomBar,
showCopyButton,
isVoiceMode,
}),
[
pendingPermissionItems.length,
showWorkingIndicator,
showBottomBar,
showCopyButton,
isVoiceMode,
]
);

View File

@@ -99,6 +99,10 @@ const userMessageStylesheet = StyleSheet.create((theme) => ({
justifyContent: "flex-end",
paddingHorizontal: theme.spacing[2],
},
content: {
alignItems: "flex-end",
maxWidth: "100%",
},
containerSpacing: {
marginBottom: theme.spacing[1],
},
@@ -123,21 +127,16 @@ const userMessageStylesheet = StyleSheet.create((theme) => ({
lineHeight: 22,
overflowWrap: "anywhere",
},
bubblePressed: {
opacity: 0.85,
},
copiedTagContainer: {
marginTop: theme.spacing[1],
marginRight: theme.spacing[4],
copyButton: {
alignSelf: "flex-end",
backgroundColor: theme.colors.surface2,
borderRadius: theme.borderRadius.base,
paddingHorizontal: theme.spacing[2],
paddingVertical: 4,
padding: theme.spacing[1],
marginTop: theme.spacing[2],
},
copiedTagText: {
color: theme.colors.foreground,
fontSize: theme.fontSize.xs,
copyButtonHidden: {
opacity: 0,
},
copyButtonVisible: {
opacity: 1,
},
}));
@@ -150,34 +149,6 @@ export const UserMessage = memo(function UserMessage({
}: UserMessageProps) {
const resolvedDisableOuterSpacing =
useDisableOuterSpacing(disableOuterSpacing);
const [copied, setCopied] = useState(false);
const copyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleLongPress = useCallback(async () => {
if (!message) {
return;
}
await Clipboard.setStringAsync(message);
setCopied(true);
if (copyTimeoutRef.current) {
clearTimeout(copyTimeoutRef.current);
}
copyTimeoutRef.current = setTimeout(() => {
setCopied(false);
copyTimeoutRef.current = null;
}, 1500);
}, [message]);
useEffect(() => {
return () => {
if (copyTimeoutRef.current) {
clearTimeout(copyTimeoutRef.current);
}
};
}, []);
return (
<View
@@ -192,26 +163,30 @@ export const UserMessage = memo(function UserMessage({
],
]}
>
<Pressable
accessibilityRole="button"
accessibilityLabel="Copy message"
accessibilityHint="Long press to copy this message"
delayLongPress={250}
onLongPress={handleLongPress}
style={({ pressed }) => [
userMessageStylesheet.bubble,
pressed ? userMessageStylesheet.bubblePressed : null,
]}
>
<Text style={userMessageStylesheet.text}>{message}</Text>
<Pressable style={userMessageStylesheet.content}>
{({ hovered }) => {
const showCopyButton = Platform.OS !== "web" || hovered;
return (
<>
<View style={userMessageStylesheet.bubble}>
<Text selectable style={userMessageStylesheet.text}>
{message}
</Text>
</View>
<TurnCopyButton
getContent={() => message}
containerStyle={[
userMessageStylesheet.copyButton,
showCopyButton
? userMessageStylesheet.copyButtonVisible
: userMessageStylesheet.copyButtonHidden,
]}
accessibilityLabel="Copy message"
/>
</>
);
}}
</Pressable>
{copied && (
<View style={userMessageStylesheet.copiedTagContainer}>
<Text style={userMessageStylesheet.copiedTagText}>
Copied to clipboard
</Text>
</View>
)}
</View>
);
});
@@ -225,7 +200,7 @@ interface AssistantMessageProps {
export const assistantMessageStylesheet = StyleSheet.create((theme) => ({
container: {
paddingHorizontal: theme.spacing[4],
paddingHorizontal: theme.spacing[2],
paddingVertical: theme.spacing[3],
},
containerSpacing: {
@@ -261,6 +236,7 @@ const turnCopyButtonStylesheet = StyleSheet.create((theme) => ({
container: {
alignSelf: "flex-start",
padding: theme.spacing[2],
paddingTop: 0,
},
iconColor: {
color: theme.colors.foregroundMuted,
@@ -272,10 +248,16 @@ const turnCopyButtonStylesheet = StyleSheet.create((theme) => ({
interface TurnCopyButtonProps {
getContent: () => string;
containerStyle?: StyleProp<ViewStyle>;
accessibilityLabel?: string;
copiedAccessibilityLabel?: string;
}
export const TurnCopyButton = memo(function TurnCopyButton({
getContent,
containerStyle,
accessibilityLabel,
copiedAccessibilityLabel,
}: TurnCopyButtonProps) {
const [copied, setCopied] = useState(false);
const copyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -310,9 +292,13 @@ export const TurnCopyButton = memo(function TurnCopyButton({
return (
<Pressable
onPress={handleCopy}
style={turnCopyButtonStylesheet.container}
style={[turnCopyButtonStylesheet.container, containerStyle]}
accessibilityRole="button"
accessibilityLabel={copied ? "Copied" : "Copy turn"}
accessibilityLabel={
copied
? (copiedAccessibilityLabel ?? "Copied")
: (accessibilityLabel ?? "Copy turn")
}
>
{({ hovered }) => {
const iconColor = hovered