app: restore per-turn copy actions and refine message copy UI
This commit is contained in:
@@ -396,14 +396,23 @@ export function AgentStreamView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const gapBelow = getGapBelow(item, index);
|
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 (
|
return (
|
||||||
<View style={[stylesheet.streamItemWrapper, { marginBottom: gapBelow }]}>
|
<View style={[stylesheet.streamItemWrapper, { marginBottom: gapBelow }]}>
|
||||||
{content}
|
{content}
|
||||||
|
{isEndOfAssistantTurn ? (
|
||||||
|
<TurnCopyButton getContent={getTurnContent} />
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[getGapBelow, renderStreamItemContent]
|
[getGapBelow, renderStreamItemContent, flatListData, agent.status]
|
||||||
);
|
);
|
||||||
|
|
||||||
const pendingPermissionItems = useMemo(
|
const pendingPermissionItems = useMemo(
|
||||||
@@ -483,18 +492,7 @@ export function AgentStreamView({
|
|||||||
}, [agentId, pendingPermissionItems.length, streamHead, streamItems]);
|
}, [agentId, pendingPermissionItems.length, streamHead, streamItems]);
|
||||||
|
|
||||||
const showWorkingIndicator = agent.status === "running";
|
const showWorkingIndicator = agent.status === "running";
|
||||||
const newestItem = flatListData[0] ?? null;
|
const showBottomBar = showWorkingIndicator || isVoiceMode;
|
||||||
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 listHeaderComponent = useMemo(() => {
|
const listHeaderComponent = useMemo(() => {
|
||||||
const hasPermissions = pendingPermissionItems.length > 0;
|
const hasPermissions = pendingPermissionItems.length > 0;
|
||||||
@@ -504,11 +502,7 @@ export function AgentStreamView({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const leftContent = showWorkingIndicator ? (
|
const leftContent = showWorkingIndicator ? <WorkingIndicator /> : null;
|
||||||
<WorkingIndicator />
|
|
||||||
) : showCopyButton ? (
|
|
||||||
<TurnCopyButton getContent={() => latestTurnContent} />
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={stylesheet.contentWrapper}>
|
<View style={stylesheet.contentWrapper}>
|
||||||
@@ -564,8 +558,6 @@ export function AgentStreamView({
|
|||||||
renderStreamItemContent,
|
renderStreamItemContent,
|
||||||
tightGap,
|
tightGap,
|
||||||
showBottomBar,
|
showBottomBar,
|
||||||
showCopyButton,
|
|
||||||
latestTurnContent,
|
|
||||||
isVoiceMode,
|
isVoiceMode,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -574,14 +566,12 @@ export function AgentStreamView({
|
|||||||
pendingPermissionCount: pendingPermissionItems.length,
|
pendingPermissionCount: pendingPermissionItems.length,
|
||||||
showWorkingIndicator,
|
showWorkingIndicator,
|
||||||
showBottomBar,
|
showBottomBar,
|
||||||
showCopyButton,
|
|
||||||
isVoiceMode,
|
isVoiceMode,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
pendingPermissionItems.length,
|
pendingPermissionItems.length,
|
||||||
showWorkingIndicator,
|
showWorkingIndicator,
|
||||||
showBottomBar,
|
showBottomBar,
|
||||||
showCopyButton,
|
|
||||||
isVoiceMode,
|
isVoiceMode,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -99,6 +99,10 @@ const userMessageStylesheet = StyleSheet.create((theme) => ({
|
|||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
paddingHorizontal: theme.spacing[2],
|
paddingHorizontal: theme.spacing[2],
|
||||||
},
|
},
|
||||||
|
content: {
|
||||||
|
alignItems: "flex-end",
|
||||||
|
maxWidth: "100%",
|
||||||
|
},
|
||||||
containerSpacing: {
|
containerSpacing: {
|
||||||
marginBottom: theme.spacing[1],
|
marginBottom: theme.spacing[1],
|
||||||
},
|
},
|
||||||
@@ -123,21 +127,16 @@ const userMessageStylesheet = StyleSheet.create((theme) => ({
|
|||||||
lineHeight: 22,
|
lineHeight: 22,
|
||||||
overflowWrap: "anywhere",
|
overflowWrap: "anywhere",
|
||||||
},
|
},
|
||||||
bubblePressed: {
|
copyButton: {
|
||||||
opacity: 0.85,
|
|
||||||
},
|
|
||||||
copiedTagContainer: {
|
|
||||||
marginTop: theme.spacing[1],
|
|
||||||
marginRight: theme.spacing[4],
|
|
||||||
alignSelf: "flex-end",
|
alignSelf: "flex-end",
|
||||||
backgroundColor: theme.colors.surface2,
|
padding: theme.spacing[1],
|
||||||
borderRadius: theme.borderRadius.base,
|
marginTop: theme.spacing[2],
|
||||||
paddingHorizontal: theme.spacing[2],
|
|
||||||
paddingVertical: 4,
|
|
||||||
},
|
},
|
||||||
copiedTagText: {
|
copyButtonHidden: {
|
||||||
color: theme.colors.foreground,
|
opacity: 0,
|
||||||
fontSize: theme.fontSize.xs,
|
},
|
||||||
|
copyButtonVisible: {
|
||||||
|
opacity: 1,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -150,34 +149,6 @@ export const UserMessage = memo(function UserMessage({
|
|||||||
}: UserMessageProps) {
|
}: UserMessageProps) {
|
||||||
const resolvedDisableOuterSpacing =
|
const resolvedDisableOuterSpacing =
|
||||||
useDisableOuterSpacing(disableOuterSpacing);
|
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 (
|
return (
|
||||||
<View
|
<View
|
||||||
@@ -192,26 +163,30 @@ export const UserMessage = memo(function UserMessage({
|
|||||||
],
|
],
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Pressable
|
<Pressable style={userMessageStylesheet.content}>
|
||||||
accessibilityRole="button"
|
{({ hovered }) => {
|
||||||
accessibilityLabel="Copy message"
|
const showCopyButton = Platform.OS !== "web" || hovered;
|
||||||
accessibilityHint="Long press to copy this message"
|
return (
|
||||||
delayLongPress={250}
|
<>
|
||||||
onLongPress={handleLongPress}
|
<View style={userMessageStylesheet.bubble}>
|
||||||
style={({ pressed }) => [
|
<Text selectable style={userMessageStylesheet.text}>
|
||||||
userMessageStylesheet.bubble,
|
{message}
|
||||||
pressed ? userMessageStylesheet.bubblePressed : null,
|
</Text>
|
||||||
]}
|
</View>
|
||||||
>
|
<TurnCopyButton
|
||||||
<Text style={userMessageStylesheet.text}>{message}</Text>
|
getContent={() => message}
|
||||||
|
containerStyle={[
|
||||||
|
userMessageStylesheet.copyButton,
|
||||||
|
showCopyButton
|
||||||
|
? userMessageStylesheet.copyButtonVisible
|
||||||
|
: userMessageStylesheet.copyButtonHidden,
|
||||||
|
]}
|
||||||
|
accessibilityLabel="Copy message"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
{copied && (
|
|
||||||
<View style={userMessageStylesheet.copiedTagContainer}>
|
|
||||||
<Text style={userMessageStylesheet.copiedTagText}>
|
|
||||||
Copied to clipboard
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -225,7 +200,7 @@ interface AssistantMessageProps {
|
|||||||
|
|
||||||
export const assistantMessageStylesheet = StyleSheet.create((theme) => ({
|
export const assistantMessageStylesheet = StyleSheet.create((theme) => ({
|
||||||
container: {
|
container: {
|
||||||
paddingHorizontal: theme.spacing[4],
|
paddingHorizontal: theme.spacing[2],
|
||||||
paddingVertical: theme.spacing[3],
|
paddingVertical: theme.spacing[3],
|
||||||
},
|
},
|
||||||
containerSpacing: {
|
containerSpacing: {
|
||||||
@@ -261,6 +236,7 @@ const turnCopyButtonStylesheet = StyleSheet.create((theme) => ({
|
|||||||
container: {
|
container: {
|
||||||
alignSelf: "flex-start",
|
alignSelf: "flex-start",
|
||||||
padding: theme.spacing[2],
|
padding: theme.spacing[2],
|
||||||
|
paddingTop: 0,
|
||||||
},
|
},
|
||||||
iconColor: {
|
iconColor: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
@@ -272,10 +248,16 @@ const turnCopyButtonStylesheet = StyleSheet.create((theme) => ({
|
|||||||
|
|
||||||
interface TurnCopyButtonProps {
|
interface TurnCopyButtonProps {
|
||||||
getContent: () => string;
|
getContent: () => string;
|
||||||
|
containerStyle?: StyleProp<ViewStyle>;
|
||||||
|
accessibilityLabel?: string;
|
||||||
|
copiedAccessibilityLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TurnCopyButton = memo(function TurnCopyButton({
|
export const TurnCopyButton = memo(function TurnCopyButton({
|
||||||
getContent,
|
getContent,
|
||||||
|
containerStyle,
|
||||||
|
accessibilityLabel,
|
||||||
|
copiedAccessibilityLabel,
|
||||||
}: TurnCopyButtonProps) {
|
}: TurnCopyButtonProps) {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const copyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const copyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
@@ -310,9 +292,13 @@ export const TurnCopyButton = memo(function TurnCopyButton({
|
|||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={handleCopy}
|
onPress={handleCopy}
|
||||||
style={turnCopyButtonStylesheet.container}
|
style={[turnCopyButtonStylesheet.container, containerStyle]}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={copied ? "Copied" : "Copy turn"}
|
accessibilityLabel={
|
||||||
|
copied
|
||||||
|
? (copiedAccessibilityLabel ?? "Copied")
|
||||||
|
: (accessibilityLabel ?? "Copy turn")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{({ hovered }) => {
|
{({ hovered }) => {
|
||||||
const iconColor = hovered
|
const iconColor = hovered
|
||||||
|
|||||||
Reference in New Issue
Block a user