Fix composer safe-area ownership

This commit is contained in:
Mohamed Boudra
2026-04-20 17:35:20 +07:00
parent 93095aa1d2
commit 067125bd4f
7 changed files with 57 additions and 37 deletions
+1 -5
View File
@@ -14,7 +14,6 @@ import {
Paperclip, Paperclip,
} from "lucide-react-native"; } from "lucide-react-native";
import Animated from "react-native-reanimated"; import Animated from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { FOOTER_HEIGHT, MAX_CONTENT_WIDTH } from "@/constants/layout"; import { FOOTER_HEIGHT, MAX_CONTENT_WIDTH } from "@/constants/layout";
import { generateMessageId, type StreamItem } from "@/types/stream"; import { generateMessageId, type StreamItem } from "@/types/stream";
@@ -169,7 +168,6 @@ export function Composer({
markScrollInvestigationRender(`Composer:${serverId}:${agentId}`); markScrollInvestigationRender(`Composer:${serverId}:${agentId}`);
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const buttonIconSize = isWeb ? theme.iconSize.md : theme.iconSize.lg; const buttonIconSize = isWeb ? theme.iconSize.md : theme.iconSize.lg;
const insets = useSafeAreaInsets();
const client = useHostRuntimeClient(serverId); const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId); const isConnected = useHostRuntimeIsConnected(serverId);
const agentDirectoryStatus = useHostRuntimeAgentDirectoryStatus(serverId); const agentDirectoryStatus = useHostRuntimeAgentDirectoryStatus(serverId);
@@ -882,9 +880,7 @@ export function Composer({
); );
return ( return (
<Animated.View <Animated.View style={[styles.container, keyboardAnimatedStyle]}>
style={[styles.container, { paddingBottom: insets.bottom }, keyboardAnimatedStyle]}
>
<AttachmentLightbox metadata={lightboxMetadata} onClose={() => setLightboxMetadata(null)} /> <AttachmentLightbox metadata={lightboxMetadata} onClose={() => setLightboxMetadata(null)} />
{/* Input area */} {/* Input area */}
<View style={[styles.inputAreaContainer, isComposerLocked && styles.inputAreaLocked]}> <View style={[styles.inputAreaContainer, isComposerLocked && styles.inputAreaLocked]}>
@@ -108,6 +108,10 @@ vi.mock("react-native-unistyles", () => {
}; };
}); });
vi.mock("react-native-safe-area-context", () => ({
useSafeAreaInsets: () => ({ top: 0, right: 0, bottom: 0, left: 0 }),
}));
function createPanelTestStyles(factory: PanelTestStyleFactory | PanelTestStyles): PanelTestStyles { function createPanelTestStyles(factory: PanelTestStyleFactory | PanelTestStyles): PanelTestStyles {
return typeof factory === "function" ? factory(theme) : factory; return typeof factory === "function" ? factory(theme) : factory;
} }
+26 -18
View File
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { ActivityIndicator, Text, View } from "react-native"; import { ActivityIndicator, Text, View } from "react-native";
import ReanimatedAnimated from "react-native-reanimated"; import ReanimatedAnimated from "react-native-reanimated";
import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { shallow, useShallow } from "zustand/shallow"; import { shallow, useShallow } from "zustand/shallow";
import { useStoreWithEqualityFn } from "zustand/traditional"; import { useStoreWithEqualityFn } from "zustand/traditional";
import invariant from "tiny-invariant"; import invariant from "tiny-invariant";
@@ -1165,6 +1166,7 @@ function ActiveAgentComposer({
onComposerHeightChange: (height: number) => void; onComposerHeightChange: (height: number) => void;
onMessageSent: () => void; onMessageSent: () => void;
}) { }) {
const insets = useSafeAreaInsets();
const agentInputDraft = useAgentInputDraft({ const agentInputDraft = useAgentInputDraft({
draftKey: buildDraftStoreKey({ draftKey: buildDraftStoreKey({
serverId, serverId,
@@ -1174,24 +1176,26 @@ function ActiveAgentComposer({
}); });
return ( return (
<Composer <View style={[styles.inputAreaWrapper, { paddingBottom: insets.bottom }]}>
agentId={agentId} <Composer
serverId={serverId} agentId={agentId}
isPaneFocused={isPaneFocused} serverId={serverId}
value={agentInputDraft.text} isPaneFocused={isPaneFocused}
onChangeText={agentInputDraft.setText} value={agentInputDraft.text}
attachments={agentInputDraft.attachments} onChangeText={agentInputDraft.setText}
onChangeAttachments={agentInputDraft.setAttachments} attachments={agentInputDraft.attachments}
cwd={agentInputDraft.cwd} onChangeAttachments={agentInputDraft.setAttachments}
clearDraft={agentInputDraft.clear} cwd={agentInputDraft.cwd}
autoFocus={isPaneFocused} clearDraft={agentInputDraft.clear}
isSubmitLoading={isSubmitLoading} autoFocus={isPaneFocused}
onAttentionInputFocus={onAttentionInputFocus} isSubmitLoading={isSubmitLoading}
onAttentionPromptSend={onAttentionPromptSend} onAttentionInputFocus={onAttentionInputFocus}
onAddImages={onAddImages} onAttentionPromptSend={onAttentionPromptSend}
onComposerHeightChange={onComposerHeightChange} onAddImages={onAddImages}
onMessageSent={onMessageSent} onComposerHeightChange={onComposerHeightChange}
/> onMessageSent={onMessageSent}
/>
</View>
); );
} }
@@ -1271,6 +1275,10 @@ const styles = StyleSheet.create((theme) => ({
content: { content: {
flex: 1, flex: 1,
}, },
inputAreaWrapper: {
width: "100%",
backgroundColor: theme.colors.surface0,
},
historySyncOverlay: { historySyncOverlay: {
position: "absolute", position: "absolute",
top: 0, top: 0,
@@ -1221,7 +1221,7 @@ function DraftAgentScreenContent({
</ScrollView> </ScrollView>
)} )}
</Animated.View> </Animated.View>
<View style={styles.inputAreaWrapper}> <View style={[styles.inputAreaWrapper, { paddingBottom: insets.bottom }]}>
<Composer <Composer
agentId={draftAgentIdRef.current} agentId={draftAgentIdRef.current}
serverId={selectedServerId ?? ""} serverId={selectedServerId ?? ""}
@@ -145,6 +145,10 @@ vi.mock("@/constants/layout", () => ({
useIsCompactFormFactor: () => false, useIsCompactFormFactor: () => false,
})); }));
vi.mock("react-native-safe-area-context", () => ({
useSafeAreaInsets: () => ({ top: 0, right: 0, bottom: 0, left: 0 }),
}));
vi.mock("lucide-react-native", () => { vi.mock("lucide-react-native", () => {
const createIcon = (name: string) => (props: Record<string, unknown>) => const createIcon = (name: string) => (props: Record<string, unknown>) =>
React.createElement("span", { ...props, "data-icon": name }); React.createElement("span", { ...props, "data-icon": name });
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Pressable, Text, View } from "react-native"; import { Pressable, Text, View } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { createNameId } from "mnemonic-id"; import { createNameId } from "mnemonic-id";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { ChevronDown, GitBranch, GitPullRequest } from "lucide-react-native"; import { ChevronDown, GitBranch, GitPullRequest } from "lucide-react-native";
@@ -12,12 +13,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region"; import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
import { SidebarMenuToggle } from "@/components/headers/menu-header"; import { SidebarMenuToggle } from "@/components/headers/menu-header";
import { ScreenHeader } from "@/components/headers/screen-header"; import { ScreenHeader } from "@/components/headers/screen-header";
import { import { HEADER_INNER_HEIGHT, MAX_CONTENT_WIDTH, useIsCompactFormFactor } from "@/constants/layout";
HEADER_INNER_HEIGHT,
HEADER_INNER_HEIGHT_MOBILE,
HEADER_TOP_PADDING_MOBILE,
MAX_CONTENT_WIDTH,
} from "@/constants/layout";
import { useToast } from "@/contexts/toast-context"; import { useToast } from "@/contexts/toast-context";
import { useAgentInputDraft } from "@/hooks/use-agent-input-draft"; import { useAgentInputDraft } from "@/hooks/use-agent-input-draft";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime"; import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
@@ -107,6 +103,8 @@ export function NewWorkspaceScreen({
displayName: displayNameProp, displayName: displayNameProp,
}: NewWorkspaceScreenProps) { }: NewWorkspaceScreenProps) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const insets = useSafeAreaInsets();
const isCompact = useIsCompactFormFactor();
const toast = useToast(); const toast = useToast();
const mergeWorkspaces = useSessionStore((state) => state.mergeWorkspaces); const mergeWorkspaces = useSessionStore((state) => state.mergeWorkspaces);
const setAgents = useSessionStore((state) => state.setAgents); const setAgents = useSessionStore((state) => state.setAgents);
@@ -453,7 +451,13 @@ export function NewWorkspaceScreen({
leftStyle={styles.headerLeft} leftStyle={styles.headerLeft}
borderless borderless
/> />
<View style={styles.content}> <View
style={[
styles.content,
isCompact ? styles.contentCompact : styles.contentCentered,
isCompact ? { paddingBottom: insets.bottom } : null,
]}
>
<TitlebarDragRegion /> <TitlebarDragRegion />
<View style={styles.centered}> <View style={styles.centered}>
<Composer <Composer
@@ -567,12 +571,14 @@ const styles = StyleSheet.create((theme) => ({
content: { content: {
position: "relative", position: "relative",
flex: 1, flex: 1,
justifyContent: "center",
alignItems: "center", alignItems: "center",
paddingBottom: { },
xs: HEADER_INNER_HEIGHT_MOBILE + HEADER_TOP_PADDING_MOBILE + theme.spacing[6], contentCentered: {
md: HEADER_INNER_HEIGHT + theme.spacing[6], justifyContent: "center",
}, paddingBottom: HEADER_INNER_HEIGHT + theme.spacing[6],
},
contentCompact: {
justifyContent: "flex-end",
}, },
centered: { centered: {
width: "100%", width: "100%",
@@ -1,6 +1,7 @@
import { useCallback, useMemo, useRef } from "react"; import { useCallback, useMemo, useRef } from "react";
import { Keyboard, ScrollView, Text, View } from "react-native"; import { Keyboard, ScrollView, Text, View } from "react-native";
import { StyleSheet } from "react-native-unistyles"; import { StyleSheet } from "react-native-unistyles";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import invariant from "tiny-invariant"; import invariant from "tiny-invariant";
import { Composer } from "@/components/composer"; import { Composer } from "@/components/composer";
import { FileDropZone } from "@/components/file-drop-zone"; import { FileDropZone } from "@/components/file-drop-zone";
@@ -48,6 +49,7 @@ export function WorkspaceDraftAgentTab({
onCreated, onCreated,
onOpenWorkspaceFile, onOpenWorkspaceFile,
}: WorkspaceDraftAgentTabProps) { }: WorkspaceDraftAgentTabProps) {
const insets = useSafeAreaInsets();
const client = useHostRuntimeClient(serverId); const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId); const isConnected = useHostRuntimeIsConnected(serverId);
const workspaceAuthority = useWorkspaceExecutionAuthority(serverId, workspaceId); const workspaceAuthority = useWorkspaceExecutionAuthority(serverId, workspaceId);
@@ -282,7 +284,7 @@ export function WorkspaceDraftAgentTab({
)} )}
</View> </View>
<View style={styles.inputAreaWrapper}> <View style={[styles.inputAreaWrapper, { paddingBottom: insets.bottom }]}>
<Composer <Composer
agentId={tabId} agentId={tabId}
serverId={serverId} serverId={serverId}