feat(sidebar): restructure layout and fix agent selection highlighting
- Move New Agent button to top header, Settings button to bottom footer - Settings screen now uses MenuHeader for consistent hamburger menu - Remove default background from agent list items (only show on hover/selected) - Fix selected agent highlighting by parsing serverId:agentId from pathname instead of using useLocalSearchParams (which doesn't update on same-pattern route navigation) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
7b55740a9e
commit
dbf9b7e75d
@@ -1,4 +1,4 @@
|
|||||||
import { Stack, useLocalSearchParams, usePathname } from "expo-router";
|
import { Stack, usePathname } from "expo-router";
|
||||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||||
import { KeyboardProvider } from "react-native-keyboard-controller";
|
import { KeyboardProvider } from "react-native-keyboard-controller";
|
||||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||||
@@ -72,17 +72,21 @@ function ProvidersWrapper({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
function AppWithSidebar({ children }: { children: ReactNode }) {
|
function AppWithSidebar({ children }: { children: ReactNode }) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const params = useLocalSearchParams<{ agentId?: string }>();
|
|
||||||
|
|
||||||
const selectedAgentId = useMemo(() => {
|
// Parse selectedAgentKey directly from pathname
|
||||||
if (pathname.startsWith("/agent/") && params.agentId) {
|
// useLocalSearchParams doesn't update when navigating between same-pattern routes
|
||||||
return params.agentId;
|
const selectedAgentKey = useMemo(() => {
|
||||||
|
// Match /agent/[serverId]/[agentId] pattern
|
||||||
|
const match = pathname.match(/^\/agent\/([^/]+)\/([^/]+)$/);
|
||||||
|
if (match) {
|
||||||
|
const [, serverId, agentId] = match;
|
||||||
|
return `${serverId}:${agentId}`;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}, [pathname, params.agentId]);
|
}, [pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContainer selectedAgentId={selectedAgentId}>{children}</AppContainer>
|
<AppContainer selectedAgentId={selectedAgentKey}>{children}</AppContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { useDaemonRegistry, type DaemonProfile } from "@/contexts/daemon-registr
|
|||||||
import { useDaemonConnections, type ConnectionStatus } from "@/contexts/daemon-connections-context";
|
import { useDaemonConnections, type ConnectionStatus } from "@/contexts/daemon-connections-context";
|
||||||
import { formatConnectionStatus, getConnectionStatusTone } from "@/utils/daemons";
|
import { formatConnectionStatus, getConnectionStatusTone } from "@/utils/daemons";
|
||||||
import { theme as defaultTheme } from "@/styles/theme";
|
import { theme as defaultTheme } from "@/styles/theme";
|
||||||
import { BackHeader } from "@/components/headers/back-header";
|
import { MenuHeader } from "@/components/headers/menu-header";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import type { UseWebSocketReturn } from "@/hooks/use-websocket";
|
import type { UseWebSocketReturn } from "@/hooks/use-websocket";
|
||||||
|
|
||||||
@@ -575,7 +575,7 @@ export default function SettingsScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<BackHeader title="Settings" />
|
<MenuHeader title="Settings" />
|
||||||
|
|
||||||
<ScrollView style={styles.scrollView}>
|
<ScrollView style={styles.scrollView}>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
|
|||||||
@@ -57,9 +57,12 @@ export function AgentList({ agents, isRefreshing = false, onRefresh, selectedAge
|
|||||||
to: "agent",
|
to: "agent",
|
||||||
params: { serverId, agentId },
|
params: { serverId, agentId },
|
||||||
});
|
});
|
||||||
|
|
||||||
const shouldReplace = pathname.startsWith("/agent/");
|
const shouldReplace = pathname.startsWith("/agent/");
|
||||||
const navigate = shouldReplace ? router.replace : router.push;
|
const navigate = shouldReplace ? router.replace : router.push;
|
||||||
|
|
||||||
onAgentSelect?.();
|
onAgentSelect?.();
|
||||||
|
|
||||||
navigate({
|
navigate({
|
||||||
pathname: "/agent/[serverId]/[agentId]",
|
pathname: "/agent/[serverId]/[agentId]",
|
||||||
params: {
|
params: {
|
||||||
@@ -91,7 +94,8 @@ export function AgentList({ agents, isRefreshing = false, onRefresh, selectedAge
|
|||||||
({ item: agent }) => {
|
({ item: agent }) => {
|
||||||
const timeAgo = formatTimeAgo(agent.lastActivityAt);
|
const timeAgo = formatTimeAgo(agent.lastActivityAt);
|
||||||
const isRunning = agent.status === "running";
|
const isRunning = agent.status === "running";
|
||||||
const isSelected = selectedAgentId === agent.id;
|
const agentKey = `${agent.serverId}:${agent.id}`;
|
||||||
|
const isSelected = selectedAgentId === agentKey;
|
||||||
const statusColor = isRunning ? "#3b82f6" : agent.requiresAttention ? "#22c55e" : null;
|
const statusColor = isRunning ? "#3b82f6" : agent.requiresAttention ? "#22c55e" : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -225,7 +229,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
paddingHorizontal: theme.spacing[3],
|
paddingHorizontal: theme.spacing[3],
|
||||||
borderRadius: theme.borderRadius.lg,
|
borderRadius: theme.borderRadius.lg,
|
||||||
marginBottom: theme.spacing[2],
|
marginBottom: theme.spacing[2],
|
||||||
backgroundColor: theme.colors.palette.zinc[950],
|
|
||||||
},
|
},
|
||||||
agentItemSelected: {
|
agentItemSelected: {
|
||||||
backgroundColor: theme.colors.palette.zinc[800],
|
backgroundColor: theme.colors.palette.zinc[800],
|
||||||
|
|||||||
@@ -234,17 +234,12 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
|||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
styles.mobileSidebar,
|
styles.mobileSidebar,
|
||||||
{ width: windowWidth, paddingTop: insets.top },
|
{ width: windowWidth, paddingTop: insets.top, paddingBottom: insets.bottom },
|
||||||
sidebarAnimatedStyle,
|
sidebarAnimatedStyle,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
{/* Header: New Agent button */}
|
||||||
<View style={styles.sidebarHeader}>
|
<View style={styles.sidebarHeader}>
|
||||||
<Pressable
|
|
||||||
style={styles.headerIconButton}
|
|
||||||
onPress={handleSettingsMobile}
|
|
||||||
>
|
|
||||||
<Settings size={20} color={theme.colors.foreground} />
|
|
||||||
</Pressable>
|
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[
|
style={[
|
||||||
styles.newAgentButton,
|
styles.newAgentButton,
|
||||||
@@ -263,6 +258,8 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Middle: scrollable agent list */}
|
||||||
<AgentList
|
<AgentList
|
||||||
agents={agents}
|
agents={agents}
|
||||||
isRefreshing={isRevalidating}
|
isRefreshing={isRevalidating}
|
||||||
@@ -270,6 +267,17 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
|||||||
selectedAgentId={selectedAgentId}
|
selectedAgentId={selectedAgentId}
|
||||||
onAgentSelect={handleAgentSelectMobile}
|
onAgentSelect={handleAgentSelectMobile}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Footer: Settings button */}
|
||||||
|
<View style={styles.sidebarFooter}>
|
||||||
|
<Pressable
|
||||||
|
style={styles.settingsButton}
|
||||||
|
onPress={handleSettingsMobile}
|
||||||
|
>
|
||||||
|
<Settings size={20} color={theme.colors.mutedForeground} />
|
||||||
|
<Text style={styles.settingsButtonText}>Settings</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</GestureDetector>
|
</GestureDetector>
|
||||||
</View>
|
</View>
|
||||||
@@ -283,10 +291,8 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.desktopSidebar, { width: DESKTOP_SIDEBAR_WIDTH }]}>
|
<View style={[styles.desktopSidebar, { width: DESKTOP_SIDEBAR_WIDTH }]}>
|
||||||
|
{/* Header: New Agent button */}
|
||||||
<View style={styles.sidebarHeader}>
|
<View style={styles.sidebarHeader}>
|
||||||
<Pressable style={styles.headerIconButton} onPress={handleSettingsDesktop}>
|
|
||||||
<Settings size={20} color={theme.colors.foreground} />
|
|
||||||
</Pressable>
|
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[
|
style={[
|
||||||
styles.newAgentButton,
|
styles.newAgentButton,
|
||||||
@@ -305,12 +311,25 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Middle: scrollable agent list */}
|
||||||
<AgentList
|
<AgentList
|
||||||
agents={agents}
|
agents={agents}
|
||||||
isRefreshing={isRevalidating}
|
isRefreshing={isRevalidating}
|
||||||
onRefresh={refreshAll}
|
onRefresh={refreshAll}
|
||||||
selectedAgentId={selectedAgentId}
|
selectedAgentId={selectedAgentId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Footer: Settings button */}
|
||||||
|
<View style={styles.sidebarFooter}>
|
||||||
|
<Pressable
|
||||||
|
style={styles.settingsButton}
|
||||||
|
onPress={handleSettingsDesktop}
|
||||||
|
>
|
||||||
|
<Settings size={20} color={theme.colors.mutedForeground} />
|
||||||
|
<Text style={styles.settingsButtonText}>Settings</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -336,19 +355,11 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
backgroundColor: theme.colors.background,
|
backgroundColor: theme.colors.background,
|
||||||
},
|
},
|
||||||
sidebarHeader: {
|
sidebarHeader: {
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
paddingHorizontal: theme.spacing[4],
|
paddingHorizontal: theme.spacing[4],
|
||||||
paddingTop: theme.spacing[4],
|
paddingTop: theme.spacing[4],
|
||||||
paddingBottom: theme.spacing[2],
|
paddingBottom: theme.spacing[2],
|
||||||
gap: theme.spacing[2],
|
|
||||||
},
|
|
||||||
headerIconButton: {
|
|
||||||
padding: theme.spacing[2],
|
|
||||||
borderRadius: theme.borderRadius.lg,
|
|
||||||
},
|
},
|
||||||
newAgentButton: {
|
newAgentButton: {
|
||||||
flex: 1,
|
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
@@ -360,4 +371,23 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
fontSize: theme.fontSize.base,
|
fontSize: theme.fontSize.base,
|
||||||
fontWeight: theme.fontWeight.normal,
|
fontWeight: theme.fontWeight.normal,
|
||||||
},
|
},
|
||||||
|
sidebarFooter: {
|
||||||
|
paddingHorizontal: theme.spacing[4],
|
||||||
|
paddingTop: theme.spacing[2],
|
||||||
|
paddingBottom: theme.spacing[4],
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: theme.colors.border,
|
||||||
|
},
|
||||||
|
settingsButton: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[3],
|
||||||
|
paddingVertical: theme.spacing[2],
|
||||||
|
paddingHorizontal: theme.spacing[2],
|
||||||
|
},
|
||||||
|
settingsButtonText: {
|
||||||
|
fontSize: theme.fontSize.base,
|
||||||
|
fontWeight: theme.fontWeight.normal,
|
||||||
|
color: theme.colors.mutedForeground,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user