refactor: redesign permission request UI with improved spacing and icons

This commit is contained in:
Mohamed Boudra
2026-02-03 09:32:42 +07:00
parent 4db2e91cdd
commit 2623328872
2 changed files with 114 additions and 63 deletions
+107 -58
View File
@@ -14,7 +14,7 @@ import {
} from "react-native"; } from "react-native";
import Markdown from "react-native-markdown-display"; import Markdown from "react-native-markdown-display";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import Animated, { import Animated, {
FadeIn, FadeIn,
@@ -27,7 +27,7 @@ import Animated, {
withSequence, withSequence,
withTiming, withTiming,
} from "react-native-reanimated"; } from "react-native-reanimated";
import { ChevronDown } from "lucide-react-native"; import { Check, ChevronDown, X } from "lucide-react-native";
import { usePanelStore } from "@/stores/panel-store"; import { usePanelStore } from "@/stores/panel-store";
import { import {
AssistantMessage, AssistantMessage,
@@ -242,6 +242,11 @@ export function AgentStreamView({
return tightGap; return tightGap;
} }
// And keep assistant messages visually connected to tool sequences (symmetry).
if (item.kind === "assistant_message" && isToolSequenceItem(aboveItem)) {
return tightGap;
}
// Different types get loose gap (16px) // Different types get loose gap (16px)
return looseGap; return looseGap;
}, },
@@ -765,6 +770,8 @@ function PermissionRequestCard({
client: DaemonClientV2 | null; client: DaemonClientV2 | null;
}) { }) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const { request } = permission; const { request } = permission;
const isPlanRequest = request.kind === "plan"; const isPlanRequest = request.kind === "plan";
@@ -987,14 +994,16 @@ function PermissionRequestCard({
{planMarkdown ? ( {planMarkdown ? (
<View style={permissionStyles.section}> <View style={permissionStyles.section}>
<Text {!isPlanRequest ? (
style={[ <Text
permissionStyles.sectionTitle, style={[
{ color: theme.colors.foregroundMuted }, permissionStyles.sectionTitle,
]} { color: theme.colors.foregroundMuted },
> ]}
{isPlanRequest ? "Plan" : "Proposed Plan"} >
</Text> Proposed plan
</Text>
) : null}
<View <View
style={[ style={[
permissionStyles.contentCard, permissionStyles.contentCard,
@@ -1024,36 +1033,27 @@ function PermissionRequestCard({
How would you like to proceed? How would you like to proceed?
</Text> </Text>
<View style={permissionStyles.optionsContainer}> <View
style={[
permissionStyles.optionsContainer,
!isMobile && permissionStyles.optionsContainerDesktop,
]}
>
<Pressable <Pressable
style={[ style={(state) => {
permissionStyles.optionButton, const hovered = Boolean((state as any).hovered);
{ backgroundColor: theme.colors.primary }, const pressed = Boolean(state.pressed);
]} return [
onPress={() => handleResponse({ behavior: "allow" })} permissionStyles.optionButton,
disabled={isResponding} {
> backgroundColor: hovered
{isResponding ? ( ? theme.colors.surface1
<ActivityIndicator : theme.colors.surface2,
size="small" borderColor: theme.colors.borderAccent,
color={theme.colors.primaryForeground} },
/> pressed ? permissionStyles.optionButtonPressed : null,
) : ( ];
<Text }}
style={[
permissionStyles.optionText,
{ color: theme.colors.primaryForeground },
]}
>
Allow
</Text>
)}
</Pressable>
<Pressable
style={[
permissionStyles.optionButton,
{ backgroundColor: theme.colors.destructive },
]}
onPress={() => onPress={() =>
handleResponse({ handleResponse({
behavior: "deny", behavior: "deny",
@@ -1063,19 +1063,54 @@ function PermissionRequestCard({
disabled={isResponding} disabled={isResponding}
> >
{isResponding ? ( {isResponding ? (
<ActivityIndicator <ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
size="small"
color={theme.colors.primaryForeground}
/>
) : ( ) : (
<Text <View style={permissionStyles.optionContent}>
style={[ <X size={14} color={theme.colors.foregroundMuted} />
permissionStyles.optionText, <Text
{ color: theme.colors.primaryForeground }, style={[
]} permissionStyles.optionText,
> { color: theme.colors.foregroundMuted },
Deny ]}
</Text> >
Deny
</Text>
</View>
)}
</Pressable>
<Pressable
style={(state) => {
const hovered = Boolean((state as any).hovered);
const pressed = Boolean(state.pressed);
return [
permissionStyles.optionButton,
{
backgroundColor: hovered
? theme.colors.surface1
: theme.colors.surface2,
borderColor: theme.colors.primary,
},
pressed ? permissionStyles.optionButtonPressed : null,
];
}}
onPress={() => handleResponse({ behavior: "allow" })}
disabled={isResponding}
>
{isResponding ? (
<ActivityIndicator size="small" color={theme.colors.primary} />
) : (
<View style={permissionStyles.optionContent}>
<Check size={14} color={theme.colors.primary} />
<Text
style={[
permissionStyles.optionText,
{ color: theme.colors.primary },
]}
>
Accept
</Text>
</View>
)} )}
</Pressable> </Pressable>
</View> </View>
@@ -1190,7 +1225,7 @@ const stylesheet = StyleSheet.create((theme) => ({
const permissionStyles = StyleSheet.create((theme) => ({ const permissionStyles = StyleSheet.create((theme) => ({
container: { container: {
marginVertical: theme.spacing[3], marginVertical: theme.spacing[3],
padding: theme.spacing[4], padding: theme.spacing[3],
borderRadius: theme.spacing[2], borderRadius: theme.spacing[2],
borderWidth: 1, borderWidth: 1,
gap: theme.spacing[2], gap: theme.spacing[2],
@@ -1208,8 +1243,6 @@ const permissionStyles = StyleSheet.create((theme) => ({
}, },
sectionTitle: { sectionTitle: {
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.xs,
textTransform: "uppercase" as const,
letterSpacing: 0.5,
}, },
contentCard: { contentCard: {
padding: theme.spacing[3], padding: theme.spacing[3],
@@ -1220,19 +1253,35 @@ const permissionStyles = StyleSheet.create((theme) => ({
}, },
question: { question: {
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.sm,
marginTop: theme.spacing[2], marginTop: theme.spacing[1],
marginBottom: theme.spacing[2], marginBottom: theme.spacing[1],
}, },
optionsContainer: { optionsContainer: {
gap: theme.spacing[2], gap: theme.spacing[2],
}, },
optionsContainerDesktop: {
flexDirection: "row",
justifyContent: "flex-end",
alignItems: "center",
width: "100%",
},
optionButton: { optionButton: {
padding: theme.spacing[3], paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[3],
borderRadius: theme.borderRadius.md, borderRadius: theme.borderRadius.md,
alignItems: "center", alignItems: "center",
borderWidth: theme.borderWidth[1],
},
optionButtonPressed: {
opacity: 0.9,
},
optionContent: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
}, },
optionText: { optionText: {
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.semibold, fontWeight: theme.fontWeight.normal,
}, },
})); }));
+7 -5
View File
@@ -180,11 +180,13 @@ export const UserMessage = memo(function UserMessage({
<View <View
style={[ style={[
userMessageStylesheet.container, userMessageStylesheet.container,
isFirstInGroup && { marginTop: theme.spacing[4] }, !resolvedDisableOuterSpacing && [
isLastInGroup && { marginBottom: theme.spacing[4] }, isFirstInGroup && { marginTop: theme.spacing[4] },
!isFirstInGroup || !isLastInGroup isLastInGroup && { marginBottom: theme.spacing[4] },
? { marginBottom: theme.spacing[1] } !isFirstInGroup || !isLastInGroup
: undefined, ? { marginBottom: theme.spacing[1] }
: undefined,
],
]} ]}
> >
<Pressable <Pressable