From f2c88ae073beb145e5e164f31a2797321427a7ce Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 6 Feb 2026 21:49:09 +0700 Subject: [PATCH] app: restore per-turn copy actions and refine message copy UI --- .../app/src/components/agent-stream-view.tsx | 34 ++---- packages/app/src/components/message.tsx | 112 ++++++++---------- 2 files changed, 61 insertions(+), 85 deletions(-) diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx index 5c84f7843..e5399b03f 100644 --- a/packages/app/src/components/agent-stream-view.tsx +++ b/packages/app/src/components/agent-stream-view.tsx @@ -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 ( {content} + {isEndOfAssistantTurn ? ( + + ) : null} ); }, - [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 ? ( - - ) : showCopyButton ? ( - latestTurnContent} /> - ) : null; + const leftContent = showWorkingIndicator ? : null; return ( @@ -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, ] ); diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index 7c9d57ade..add118136 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -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 | 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 ( - [ - userMessageStylesheet.bubble, - pressed ? userMessageStylesheet.bubblePressed : null, - ]} - > - {message} + + {({ hovered }) => { + const showCopyButton = Platform.OS !== "web" || hovered; + return ( + <> + + + {message} + + + message} + containerStyle={[ + userMessageStylesheet.copyButton, + showCopyButton + ? userMessageStylesheet.copyButtonVisible + : userMessageStylesheet.copyButtonHidden, + ]} + accessibilityLabel="Copy message" + /> + + ); + }} - {copied && ( - - - Copied to clipboard - - - )} ); }); @@ -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; + accessibilityLabel?: string; + copiedAccessibilityLabel?: string; } export const TurnCopyButton = memo(function TurnCopyButton({ getContent, + containerStyle, + accessibilityLabel, + copiedAccessibilityLabel, }: TurnCopyButtonProps) { const [copied, setCopied] = useState(false); const copyTimeoutRef = useRef | null>(null); @@ -310,9 +292,13 @@ export const TurnCopyButton = memo(function TurnCopyButton({ return ( {({ hovered }) => { const iconColor = hovered