diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx
index 588b3cf71..d53840b3e 100644
--- a/packages/app/src/app/_layout.tsx
+++ b/packages/app/src/app/_layout.tsx
@@ -6,9 +6,7 @@ import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import { RealtimeProvider } from "@/contexts/realtime-context";
import { useAppSettings } from "@/hooks/use-settings";
import { View, ActivityIndicator, Text } from "react-native";
-import { GlobalFooter } from "@/components/global-footer";
import { useUnistyles } from "react-native-unistyles";
-import { FooterControlsProvider } from "@/contexts/footer-controls-context";
import { DaemonRegistryProvider, useDaemonRegistry } from "@/contexts/daemon-registry-context";
import { DaemonConnectionsProvider } from "@/contexts/daemon-connections-context";
import { MultiDaemonSessionHost } from "@/components/multi-daemon-session-host";
@@ -111,29 +109,26 @@ export default function RootLayout() {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/app/src/app/agent/[serverId]/[agentId].tsx b/packages/app/src/app/agent/[serverId]/[agentId].tsx
index cb6534c1d..b573ee57c 100644
--- a/packages/app/src/app/agent/[serverId]/[agentId].tsx
+++ b/packages/app/src/app/agent/[serverId]/[agentId].tsx
@@ -19,7 +19,6 @@ import { BackHeader } from "@/components/headers/back-header";
import { AgentStreamView } from "@/components/agent-stream-view";
import { AgentInputArea } from "@/components/agent-input-area";
import { CreateAgentModal, ImportAgentModal, type CreateAgentInitialValues } from "@/components/create-agent-modal";
-import { useFooterControls } from "@/contexts/footer-controls-context";
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import type { ConnectionStatus } from "@/contexts/daemon-connections-context";
import { formatConnectionStatus } from "@/utils/daemons";
@@ -177,7 +176,6 @@ function AgentScreenContent({ serverId, agentId, onBack }: AgentScreenContentPro
const { theme } = useUnistyles();
const insets = useSafeAreaInsets();
const router = useRouter();
- const { registerFooterControls, unregisterFooterControls } = useFooterControls();
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
const [menuVisible, setMenuVisible] = useState(false);
const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 });
@@ -348,24 +346,6 @@ function AgentScreenContent({ serverId, agentId, onBack }: AgentScreenContentPro
initializeAgent({ agentId: resolvedAgentId });
}, [resolvedAgentId, initializeAgent, isInitializingFromMap]);
- const agentControls = useMemo(() => {
- if (!resolvedAgentId) return null;
- return ;
- }, [resolvedAgentId, serverId]);
-
- useEffect(() => {
- if (!agentControls || !agent || isInitializing) {
- unregisterFooterControls();
- return;
- }
-
- registerFooterControls(agentControls);
-
- return () => {
- unregisterFooterControls();
- };
- }, [agentControls, agent, isInitializing, registerFooterControls, unregisterFooterControls]);
-
const recalculateMenuPosition = useCallback(
(onMeasured?: () => void) => {
requestAnimationFrame(() => {
@@ -567,6 +547,11 @@ function AgentScreenContent({ serverId, agentId, onBack }: AgentScreenContentPro
+ {/* Agent Input Area */}
+ {!isInitializing && agent && resolvedAgentId && (
+
+ )}
+
{/* Dropdown Menu */}
+ {/* Home Footer */}
+
+
{/* Create Agent Modal */}
state.sessions[serverId]?.ws);
@@ -825,6 +829,15 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
};
});
+ const keyboardAnimatedStyle = useAnimatedStyle(() => {
+ "worklet";
+ const absoluteHeight = Math.abs(keyboardHeight.value);
+ const shift = Math.max(0, absoluteHeight - insets.bottom);
+ return {
+ transform: [{ translateY: -shift }],
+ };
+ });
+
async function handleRealtimePress() {
try {
if (isRealtimeMode) {
@@ -923,7 +936,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
);
return (
-
+
{/* Border separator */}
@@ -1104,7 +1117,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
) : null}
-
+
);
}
diff --git a/packages/app/src/components/global-footer.tsx b/packages/app/src/components/home-footer.tsx
similarity index 91%
rename from packages/app/src/components/global-footer.tsx
rename to packages/app/src/components/home-footer.tsx
index 7fe1db851..9155a6aee 100644
--- a/packages/app/src/components/global-footer.tsx
+++ b/packages/app/src/components/home-footer.tsx
@@ -1,12 +1,12 @@
import { useCallback, useMemo, useState } from "react";
import { View, Pressable, Text, Platform, Modal, Alert } from "react-native";
-import { usePathname, useRouter } from "expo-router";
+import { useRouter } from "expo-router";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { AudioLines, Users, Plus, Download } from "lucide-react-native";
import { useRealtime } from "@/contexts/realtime-context";
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
-import { useFooterControls, FOOTER_HEIGHT } from "@/contexts/footer-controls-context";
+import { FOOTER_HEIGHT } from "@/constants/layout";
import { RealtimeControls } from "./realtime-controls";
import { CreateAgentModal, ImportAgentModal } from "./create-agent-modal";
import Animated, {
@@ -16,14 +16,12 @@ import Animated, {
} from "react-native-reanimated";
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
-export function GlobalFooter() {
+export function HomeFooter() {
const { theme } = useUnistyles();
const insets = useSafeAreaInsets();
- const pathname = usePathname();
const router = useRouter();
const { isRealtimeMode, startRealtime } = useRealtime();
const { connectionStates } = useDaemonConnections();
- const { controls } = useFooterControls();
const [showCreateModal, setShowCreateModal] = useState(false);
const [showImportModal, setShowImportModal] = useState(false);
const [showRealtimeHostPicker, setShowRealtimeHostPicker] = useState(false);
@@ -33,12 +31,6 @@ export function GlobalFooter() {
const realtimeFadeIn = shouldDisableEntryExitAnimations ? undefined : FadeIn.duration(250);
const realtimeFadeOut = shouldDisableEntryExitAnimations ? undefined : FadeOut.duration(250);
- // Determine current screen type
- const isAgentScreen = pathname?.startsWith("/agent/");
-
- const hasRegisteredControls = !!controls;
- const showAgentControls = isAgentScreen && hasRegisteredControls;
-
const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
const bottomInset = insets.bottom;
@@ -76,7 +68,7 @@ export function GlobalFooter() {
}
if (realtimeEligibleHosts.length === 1) {
void startRealtime(realtimeEligibleHosts[0].daemon.id).catch((error) => {
- console.error("[GlobalFooter] Failed to start realtime", error);
+ console.error("[HomeFooter] Failed to start realtime", error);
Alert.alert("Realtime failed", "Unable to start realtime mode for this host.");
});
return;
@@ -88,7 +80,7 @@ export function GlobalFooter() {
(daemonId: string) => {
setShowRealtimeHostPicker(false);
void startRealtime(daemonId).catch((error) => {
- console.error("[GlobalFooter] Failed to start realtime", error);
+ console.error("[HomeFooter] Failed to start realtime", error);
Alert.alert("Realtime failed", "Unable to start realtime mode for this host.");
});
},
@@ -99,26 +91,6 @@ export function GlobalFooter() {
setShowRealtimeHostPicker(false);
}, []);
- if (showAgentControls) {
- return (
-
- {controls}
-
- );
- }
-
- if (isAgentScreen) {
- return null;
- }
-
// For home and orchestrator screens, show action buttons with realtime stacked on top
const nonAgentFooterHeight = isRealtimeMode
? FOOTER_HEIGHT * 2 + insets.bottom
@@ -191,7 +163,7 @@ export function GlobalFooter() {
{
- console.log("[GlobalFooter] New Agent button pressed");
+ console.log("[HomeFooter] New Agent button pressed");
setShowCreateModal(true);
}}
style={({ pressed }) => [
diff --git a/packages/app/src/components/realtime-controls.tsx b/packages/app/src/components/realtime-controls.tsx
index 535ed35c7..234351255 100644
--- a/packages/app/src/components/realtime-controls.tsx
+++ b/packages/app/src/components/realtime-controls.tsx
@@ -3,7 +3,7 @@ import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { MicOff, Square } from "lucide-react-native";
import { VolumeMeter } from "./volume-meter";
import { useRealtime } from "@/contexts/realtime-context";
-import { FOOTER_HEIGHT } from "@/contexts/footer-controls-context";
+import { FOOTER_HEIGHT } from "@/constants/layout";
const CONTROL_BUTTON_SIZE = 48;
const VERTICAL_PADDING = (FOOTER_HEIGHT - CONTROL_BUTTON_SIZE) / 2;
diff --git a/packages/app/src/components/voice-note-recording-overlay.tsx b/packages/app/src/components/voice-note-recording-overlay.tsx
index 77886d7d1..a9e8f3b96 100644
--- a/packages/app/src/components/voice-note-recording-overlay.tsx
+++ b/packages/app/src/components/voice-note-recording-overlay.tsx
@@ -2,7 +2,7 @@ import { View, Text, Pressable, ActivityIndicator } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { X, ArrowUp, RefreshCcw } from "lucide-react-native";
import { VolumeMeter } from "./volume-meter";
-import { FOOTER_HEIGHT } from "@/contexts/footer-controls-context";
+import { FOOTER_HEIGHT } from "@/constants/layout";
import type { DictationStatus } from "@/hooks/use-dictation";
interface VoiceNoteRecordingOverlayProps {
diff --git a/packages/app/src/constants/layout.ts b/packages/app/src/constants/layout.ts
new file mode 100644
index 000000000..77097f884
--- /dev/null
+++ b/packages/app/src/constants/layout.ts
@@ -0,0 +1 @@
+export const FOOTER_HEIGHT = 75;
diff --git a/packages/app/src/contexts/footer-controls-context.tsx b/packages/app/src/contexts/footer-controls-context.tsx
deleted file mode 100644
index 5d15330ea..000000000
--- a/packages/app/src/contexts/footer-controls-context.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { createContext, useCallback, useContext, useMemo, useState } from "react";
-import type { ReactNode } from "react";
-
-export const FOOTER_HEIGHT = 75;
-
-interface FooterControlsContextValue {
- controls: ReactNode | null;
- registerFooterControls: (controls: ReactNode) => void;
- unregisterFooterControls: () => void;
-}
-
-const FooterControlsContext = createContext(undefined);
-
-export function FooterControlsProvider({ children }: { children: ReactNode }) {
- const [controls, setControls] = useState(null);
-
- const registerFooterControls = useCallback((nextControls: ReactNode) => {
- setControls(nextControls);
- }, []);
-
- const unregisterFooterControls = useCallback(() => {
- setControls(null);
- }, []);
-
- const value = useMemo(
- () => ({
- controls,
- registerFooterControls,
- unregisterFooterControls,
- }),
- [controls, registerFooterControls, unregisterFooterControls],
- );
-
- return (
-
- {children}
-
- );
-}
-
-export function useFooterControls() {
- const context = useContext(FooterControlsContext);
-
- if (!context) {
- throw new Error("useFooterControls must be used within a FooterControlsProvider");
- }
-
- return context;
-}