From 843d9234bd611b8be453fd75485a0a37c1397e47 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 26 Apr 2026 11:11:42 +0700 Subject: [PATCH] refactor: tighten tool call badge hover affordances Move the open-file button inline after the summary text so the click target sits next to what it acts on, and replace the tool icon with the chevron on hover instead of showing a separate chevron at the row's end. Hovering the icon slot keeps the tool icon visible for predictability. --- packages/app/src/components/message.tsx | 92 ++++++++++++++++++------- 1 file changed, 69 insertions(+), 23 deletions(-) diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index 0e1a47ac0..6710e4f83 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -997,7 +997,8 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({ opacity: 0.72, }, secondaryLabel: { - flex: 1, + flexShrink: 1, + minWidth: 0, color: theme.colors.foregroundMuted, fontSize: theme.fontSize.base, fontWeight: theme.fontWeight.normal, @@ -2064,7 +2065,7 @@ function ExpandableBadgeSecondaryLabel({ onSecondaryLayout, }: ExpandableBadgeSecondaryLabelProps) { if (!secondaryLabel) { - return ; + return null; } return ( void; onLabelLayout: (event: LayoutChangeEvent) => void; onSecondaryLayout: (event: LayoutChangeEvent) => void; + showOpenFileButton: boolean; + isOpenFileHovered: boolean; + onOpenFilePress: (event: GestureResponderEvent) => void; + onOpenFileHoverIn: () => void; + onOpenFileHoverOut: () => void; } function ExpandableBadgeLabelRow({ @@ -2146,7 +2152,13 @@ function ExpandableBadgeLabelRow({ onLabelRowLayout, onLabelLayout, onSecondaryLayout, + showOpenFileButton, + isOpenFileHovered, + onOpenFilePress, + onOpenFileHoverIn, + onOpenFileHoverOut, }: ExpandableBadgeLabelRowProps) { + const { theme } = useUnistyles(); return ( + {showOpenFileButton ? ( + + + + ) : null} {isWebShimmer ? ( ; + iconNode: ReactNode; +}): ReactNode { + if (showChevron) { + return ; + } + return iconNode; +} + function computeShimmerMetrics(input: { label: string; secondaryLabel: string | undefined; @@ -2343,6 +2389,7 @@ const ExpandableBadge = memo(function ExpandableBadge({ const resolvedDisableOuterSpacing = useDisableOuterSpacing(disableOuterSpacing); const [isHovered, setIsHovered] = useState(false); const [isOpenFileHovered, setIsOpenFileHovered] = useState(false); + const [isIconHovered, setIsIconHovered] = useState(false); const [isPressed, setIsPressed] = useState(false); const isInteractive = Boolean(onToggle); const hasDetailContent = Boolean(renderDetails); @@ -2371,6 +2418,8 @@ const ExpandableBadge = memo(function ExpandableBadge({ ); const handleOpenFileHoverIn = useCallback(() => setIsOpenFileHovered(true), []); const handleOpenFileHoverOut = useCallback(() => setIsOpenFileHovered(false), []); + const handleIconHoverIn = useCallback(() => setIsIconHovered(true), []); + const handleIconHoverOut = useCallback(() => setIsIconHovered(false), []); const nativeGradientIdRef = useRef( `shimmer-gradient-${Math.random().toString(36).substring(2, 9)}`, @@ -2567,6 +2616,12 @@ const ExpandableBadge = memo(function ExpandableBadge({ const iconColor = resolveExpandableBadgeIconColor({ isError, isActive, theme }); const iconNode = renderExpandableBadgeIcon({ isError, iconColor, icon }); + const iconSlotNode = renderExpandableBadgeIconSlot({ + showChevron: isInteractive && isHovered && !isIconHovered, + chevronColor: theme.colors.foreground, + chevronStyle, + iconNode, + }); const pressHandlers = isInteractive ? { @@ -2591,7 +2646,13 @@ const ExpandableBadge = memo(function ExpandableBadge({ style={pressableStyle} > - {iconNode} + + {iconSlotNode} + - {onOpenFile && isHovered ? ( - - - - ) : null} - {isInteractive && isHovered ? ( - - ) : null} {detailContent ? (