diff --git a/packages/app/src/app/agent/[id].tsx b/packages/app/src/app/agent/[id].tsx
index f8a34f253..3fef38b4d 100644
--- a/packages/app/src/app/agent/[id].tsx
+++ b/packages/app/src/app/agent/[id].tsx
@@ -1,5 +1,13 @@
import { useEffect, useMemo, useRef, useCallback, useState } from "react";
-import { View, Text, ActivityIndicator, Pressable, Modal, Dimensions } from "react-native";
+import {
+ View,
+ Text,
+ ActivityIndicator,
+ Pressable,
+ Modal,
+ useWindowDimensions,
+ LayoutChangeEvent,
+} from "react-native";
import { useLocalSearchParams, useRouter } from "expo-router";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
@@ -28,8 +36,10 @@ export default function AgentScreen() {
initializeAgent,
} = useSession();
const { registerFooterControls, unregisterFooterControls } = useFooterControls();
+ const { width: windowWidth, height: windowHeight } = useWindowDimensions();
const [menuVisible, setMenuVisible] = useState(false);
const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 });
+ const [menuContentHeight, setMenuContentHeight] = useState(0);
const menuButtonRef = useRef(null);
// Keyboard animation
@@ -97,25 +107,75 @@ export default function AgentScreen() {
};
}, [agentControls, agent, isInitializing, registerFooterControls, unregisterFooterControls]);
- const handleOpenMenu = useCallback(() => {
- menuButtonRef.current?.measureInWindow((x, y, width, height) => {
- const screenWidth = Dimensions.get("window").width;
- const verticalOffset = 6;
- const horizontalMargin = 16;
- const desiredLeft = x + width - DROPDOWN_WIDTH;
- const maxLeft = screenWidth - DROPDOWN_WIDTH - horizontalMargin;
- const clampedLeft = Math.min(Math.max(desiredLeft, horizontalMargin), maxLeft);
+ const recalculateMenuPosition = useCallback(
+ (onMeasured?: () => void) => {
+ requestAnimationFrame(() => {
+ const anchor = menuButtonRef.current;
- setMenuPosition({
- top: y + height + verticalOffset,
- left: clampedLeft,
+ if (!anchor) {
+ if (onMeasured) {
+ onMeasured();
+ }
+ return;
+ }
+
+ anchor.measureInWindow((x, y, width, height) => {
+ const verticalOffset = 8;
+ const horizontalMargin = 16;
+ const desiredLeft = x + width - DROPDOWN_WIDTH;
+ const maxLeft = windowWidth - DROPDOWN_WIDTH - horizontalMargin;
+ const clampedLeft = Math.min(Math.max(desiredLeft, horizontalMargin), maxLeft);
+
+ // Position menu below button - add insets.top to account for status bar
+ const buttonBottom = y + height + insets.top;
+ const top = buttonBottom + verticalOffset;
+
+ // If menu would go off screen, clamp to visible area
+ const bottomEdge = top + menuContentHeight;
+ const maxBottom = windowHeight - horizontalMargin;
+ const clampedTop = bottomEdge > maxBottom
+ ? Math.max(verticalOffset, maxBottom - menuContentHeight)
+ : top;
+
+ console.log('[Menu] Button position:', { x, y, width, height, insetsTop: insets.top });
+ console.log('[Menu] Calculated position:', { buttonBottom, top, clampedTop, left: clampedLeft });
+
+ setMenuPosition({
+ top: clampedTop,
+ left: clampedLeft,
+ });
+
+ if (onMeasured) {
+ onMeasured();
+ }
+ });
});
+ },
+ [menuContentHeight, windowHeight, windowWidth]
+ );
+
+ const handleOpenMenu = useCallback(() => {
+ recalculateMenuPosition(() => {
setMenuVisible(true);
});
- }, []);
+ }, [recalculateMenuPosition]);
const handleCloseMenu = useCallback(() => {
setMenuVisible(false);
+ setMenuContentHeight(0);
+ }, []);
+
+ useEffect(() => {
+ if (!menuVisible) {
+ return;
+ }
+
+ recalculateMenuPosition();
+ }, [menuVisible, recalculateMenuPosition]);
+
+ const handleMenuLayout = useCallback((event: LayoutChangeEvent) => {
+ const { height } = event.nativeEvent.layout;
+ setMenuContentHeight((current) => (current === height ? current : height));
}, []);
const handleViewChanges = useCallback(() => {
@@ -191,6 +251,7 @@ export default function AgentScreen() {
width: DROPDOWN_WIDTH,
},
]}
+ onLayout={handleMenuLayout}
>
diff --git a/packages/app/src/app/git-diff.tsx b/packages/app/src/app/git-diff.tsx
index e60ae139f..8cc137200 100644
--- a/packages/app/src/app/git-diff.tsx
+++ b/packages/app/src/app/git-diff.tsx
@@ -134,18 +134,28 @@ export default function GitDiffScreen() {
>
{file.lines.map((line, lineIndex) => (
-
- {line.content}
-
+
+ {line.content}
+
+
))}
@@ -232,27 +242,42 @@ const styles = StyleSheet.create((theme) => ({
diffLinesContainer: {
alignSelf: "flex-start",
},
- diffLine: {
- fontSize: theme.fontSize.xs,
- fontFamily: "monospace",
+ diffLineContainer: {
+ minWidth: "100%",
paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[1],
- flexShrink: 0,
- minWidth: "100%",
+ alignSelf: "flex-start",
+ flexDirection: "row",
+ alignItems: "center",
},
- addLine: {
+ diffLineText: {
+ fontSize: theme.fontSize.xs,
+ fontFamily: "monospace",
+ color: theme.colors.foreground,
+ flexShrink: 0,
+ },
+ addLineContainer: {
backgroundColor: theme.colors.palette.green[900],
+ },
+ addLineText: {
color: theme.colors.palette.green[200],
},
- removeLine: {
+ removeLineContainer: {
backgroundColor: theme.colors.palette.red[900],
+ },
+ removeLineText: {
color: theme.colors.palette.red[200],
},
- headerLine: {
- color: theme.colors.mutedForeground,
+ headerLineContainer: {
backgroundColor: theme.colors.muted,
},
- contextLine: {
+ headerLineText: {
+ color: theme.colors.mutedForeground,
+ },
+ contextLineContainer: {
+ backgroundColor: theme.colors.card,
+ },
+ contextLineText: {
color: theme.colors.mutedForeground,
},
}));
diff --git a/packages/app/src/components/agent-input-area.tsx b/packages/app/src/components/agent-input-area.tsx
index 1f0cdb147..f73dc2630 100644
--- a/packages/app/src/components/agent-input-area.tsx
+++ b/packages/app/src/components/agent-input-area.tsx
@@ -261,11 +261,6 @@ export function AgentInputArea({ agentId }: AgentInputAreaProps) {
return (
- {/* Status bar */}
-
-
-
-
{/* Border separator */}
@@ -298,10 +293,26 @@ export function AgentInputArea({ agentId }: AgentInputAreaProps) {
)}
- {/* Input row */}
-
- {/* Attachment button */}
- {!isRecording && (
+ {/* Full-width text input */}
+ = MAX_INPUT_HEIGHT}
+ onContentSizeChange={handleContentSizeChange}
+ editable={!isRecording && ws.isConnected}
+ />
+
+ {/* Button row below input */}
+
+ {/* Left button group */}
+
- )}
+
+
- {/* Text input */}
- = MAX_INPUT_HEIGHT}
- onContentSizeChange={handleContentSizeChange}
- editable={!isRecording && ws.isConnected}
- />
-
- {/* Buttons */}
-
- {hasText || hasImages ? (
- // Send button when text is entered or images are selected
+ {/* Right button group */}
+
+ {hasText || hasImages ? (
+
+
+
+ ) : !isRealtimeMode ? (
+ <>
-
+ {isRecording ? (
+
+ ) : (
+
+ )}
- ) : !isRealtimeMode ? (
- // Voice and Realtime buttons when no text and not in realtime mode
- <>
- {/* Voice recording button */}
-
- {isRecording ? (
-
- ) : (
-
- )}
-
- {/* Realtime button */}
-
-
-
- >
- ) : null}
+
+
+
+ >
+ ) : null}
@@ -400,9 +392,6 @@ const styles = StyleSheet.create((theme) => ({
container: {
flexDirection: "column",
},
- statusBarContainer: {
- backgroundColor: theme.colors.background,
- },
borderSeparator: {
height: theme.borderWidth[1],
backgroundColor: theme.colors.border,
@@ -417,8 +406,8 @@ const styles = StyleSheet.create((theme) => ({
inputContainer: {
flexDirection: "column",
paddingHorizontal: theme.spacing[4],
- paddingVertical: BASE_VERTICAL_PADDING,
- gap: theme.spacing[2],
+ paddingVertical: theme.spacing[3],
+ gap: theme.spacing[3],
minHeight: FOOTER_HEIGHT,
},
overlayContainer: {
@@ -452,7 +441,26 @@ const styles = StyleSheet.create((theme) => ({
alignItems: "center",
justifyContent: "center",
},
- inputRow: {
+ textInput: {
+ width: "100%",
+ paddingHorizontal: theme.spacing[4],
+ paddingVertical: theme.spacing[3],
+ backgroundColor: theme.colors.muted,
+ borderRadius: theme.borderRadius.lg,
+ color: theme.colors.foreground,
+ fontSize: theme.fontSize.base,
+ },
+ buttonRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ },
+ leftButtonGroup: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: theme.spacing[2],
+ },
+ rightButtonGroup: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
@@ -463,20 +471,6 @@ const styles = StyleSheet.create((theme) => ({
alignItems: "center",
justifyContent: "center",
},
- textInput: {
- flex: 1,
- paddingHorizontal: theme.spacing[4],
- paddingVertical: theme.spacing[2],
- backgroundColor: theme.colors.muted,
- borderRadius: theme.borderRadius.lg,
- color: theme.colors.foreground,
- fontSize: theme.fontSize.base,
- },
- buttonRow: {
- flexDirection: "row",
- alignItems: "center",
- gap: theme.spacing[2],
- },
sendButton: {
width: 40,
height: 40,
diff --git a/packages/app/src/components/agent-status-bar.tsx b/packages/app/src/components/agent-status-bar.tsx
index 9a7ecd4e0..df78af342 100644
--- a/packages/app/src/components/agent-status-bar.tsx
+++ b/packages/app/src/components/agent-status-bar.tsx
@@ -78,16 +78,13 @@ export function AgentStatusBar({ agentId }: AgentStatusBarProps) {
)}
- {/* Agent Status Indicator */}
-
-
- {getStatusLabel(agent.status)}
-
+ {/* Agent Status Indicator - just a dot */}
+
{/* Mode selector modal */}
({
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[3],
- paddingHorizontal: theme.spacing[4],
- paddingTop: theme.spacing[2],
- paddingBottom: theme.spacing[1],
},
modeBadge: {
flexDirection: "row",