fix: read project scope from router params; align sidebar header icons

- useSelectMention now reads the active project from React Router's search
  params instead of window.location, which can drift out of sync because
  new-chat params are written to the URL via raw history.pushState; the
  Model Selector and @ command now reliably keep the project on switch
- Move the Chats section header out of the virtualized list so it renders
  in the same context as the Projects header and isn't shifted by the
  list scrollbar
- Inset header action icons (pr-2) so Projects/Chats header icons line up
  with the project-row and conversation-row trailing actions
- Extract getRouteChatProjectId into utils for the submit path
This commit is contained in:
Danny Avila 2026-06-03 09:36:03 -04:00
parent e1e1ebb825
commit e185e2ce0a
5 changed files with 49 additions and 48 deletions

View file

@ -102,7 +102,7 @@ const ChatsHeader: FC<ChatsHeaderProps> = memo(({ isExpanded, onToggle }) => {
}, [conversation?.conversationId, newConversation, queryClient]);
return (
<div className="flex h-8 w-full items-center gap-0.5">
<div className="flex h-8 w-full items-center gap-0.5 pr-2">
<button
onClick={onToggle}
className="group flex min-w-0 flex-1 items-center gap-1 rounded-lg px-1 py-2 text-xs font-bold text-text-secondary outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-black dark:focus-visible:ring-white"
@ -156,7 +156,6 @@ DateLabel.displayName = 'DateLabel';
type FlattenedItem =
| { type: 'favorites' }
| { type: 'chats-header' }
| { type: 'header'; groupName: string }
| { type: 'convo'; convo: TConversation }
| { type: 'loading' };
@ -244,7 +243,6 @@ const Conversations: FC<ConversationsProps> = ({
if (shouldShowFavorites) {
items.push({ type: 'favorites' });
}
items.push({ type: 'chats-header' });
if (isChatsExpanded) {
groupedConversations.forEach(([groupName, convos]) => {
@ -277,9 +275,6 @@ const Conversations: FC<ConversationsProps> = ({
if (item.type === 'favorites') {
return `favorites-${favoritesContentKeyRef.current}`;
}
if (item.type === 'chats-header') {
return 'chats-header';
}
if (item.type === 'header') {
return `header-${item.groupName}`;
}
@ -342,22 +337,9 @@ const Conversations: FC<ConversationsProps> = ({
);
}
if (item.type === 'chats-header') {
return (
<MeasuredRow key={key} {...rowProps}>
<ChatsHeader
isExpanded={isChatsExpanded}
onToggle={() => setIsChatsExpanded(!isChatsExpanded)}
/>
</MeasuredRow>
);
}
if (item.type === 'header') {
// First date header index depends on whether favorites row is included
// With favorites: [favorites, chats-header, first-header] → index 2
// Without favorites: [chats-header, first-header] → index 1
const firstHeaderIndex = shouldShowFavorites ? 2 : 1;
// First date header index depends on whether the favorites row is included
const firstHeaderIndex = shouldShowFavorites ? 1 : 0;
return (
<MeasuredRow key={key} {...rowProps}>
<DateLabel groupName={item.groupName} isFirst={index === firstHeaderIndex} />
@ -381,17 +363,7 @@ const Conversations: FC<ConversationsProps> = ({
return null;
},
[
cache,
flattenedItems,
moveToTop,
toggleNav,
isSmallScreen,
isChatsExpanded,
setIsChatsExpanded,
shouldShowFavorites,
activeJobIds,
],
[cache, flattenedItems, moveToTop, toggleNav, isSmallScreen, shouldShowFavorites, activeJobIds],
);
const getRowHeight = useCallback(
@ -415,6 +387,12 @@ const Conversations: FC<ConversationsProps> = ({
return (
<div className="relative flex h-full min-h-0 flex-col pb-2 text-sm text-text-primary">
<div className="px-3">
<ChatsHeader
isExpanded={isChatsExpanded}
onToggle={() => setIsChatsExpanded(!isChatsExpanded)}
/>
</div>
{isSearchLoading ? (
<div className="flex flex-1 items-center justify-center">
<Spinner className="text-text-primary" />

View file

@ -452,7 +452,7 @@ const ProjectsSection = ({ toggleNav, isAuthenticated }: ProjectsSectionProps) =
return (
<div className="flex flex-col px-3 text-sm">
<div className="flex h-8 w-full items-center gap-0.5">
<div className="flex h-8 w-full items-center gap-0.5 pr-2">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="group flex min-w-0 flex-1 items-center gap-1 rounded-lg px-1 py-2 text-xs font-bold text-text-secondary outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-black dark:focus-visible:ring-white"

View file

@ -28,7 +28,7 @@ import type { SetterOrUpdater } from 'recoil';
import type { TAskFunction, ExtendedFile } from '~/common';
import useSetFilesToDelete from '~/hooks/Files/useSetFilesToDelete';
import useGetSender from '~/hooks/Conversations/useGetSender';
import { logger, createDualMessageContent } from '~/utils';
import { logger, createDualMessageContent, getRouteChatProjectId } from '~/utils';
import store, { useGetEphemeralAgent } from '~/store';
import { startupConfigKey } from '~/data-provider';
import useUserKey from '~/hooks/Input/useUserKey';
@ -40,15 +40,6 @@ const logChatRequest = (request: Record<string, unknown>) => {
logger.log('=====================================');
};
const getRouteChatProjectId = () => {
if (typeof window === 'undefined') {
return null;
}
const projectId = new URLSearchParams(window.location.search).get('projectId');
return projectId != null && /^[a-f\d]{24}$/i.test(projectId) ? projectId : null;
};
export default function useChatFunctions({
index = 0,
files,

View file

@ -1,5 +1,6 @@
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { useSearchParams } from 'react-router-dom';
import { EModelEndpoint, isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import type {
TPreset,
@ -39,6 +40,16 @@ export default function useSelectMention({
const getDefaultConversation = useDefaultConvo();
const modularChat = useRecoilValue(store.modularChat);
const availableTools = useRecoilValue(store.availableTools);
const [searchParams] = useSearchParams();
/**
* Project scope is read from React Router's search params (the same source the
* project chip reads), not window.location new-chat params are written to the
* URL via raw history.pushState, so window.location can drift out of sync.
*/
const routeChatProjectId = useMemo(() => {
const projectId = searchParams.get('projectId');
return projectId != null && /^[a-f\d]{24}$/i.test(projectId) ? projectId : null;
}, [searchParams]);
const onSelectSpec = useCallback(
(spec?: TModelSpec) => {
@ -109,7 +120,7 @@ export default function useSelectMention({
newConversation({
template: {
...(template as Partial<TConversation>),
chatProjectId: conversation?.chatProjectId ?? null,
chatProjectId: routeChatProjectId,
},
preset,
keepAddedConvos: isModular,
@ -122,6 +133,7 @@ export default function useSelectMention({
newConversation,
endpointsConfig,
assistantsMap,
routeChatProjectId,
],
);
@ -206,13 +218,20 @@ export default function useSelectMention({
newConversation({
template: {
...(template as Partial<TConversation>),
chatProjectId: conversation?.chatProjectId ?? null,
chatProjectId: routeChatProjectId,
},
preset: { ...kwargs, spec: null, iconURL: null, modelLabel: null, endpoint: newEndpoint },
keepAddedConvos: isNewModular,
});
},
[getConversation, getDefaultConversation, modularChat, newConversation, endpointsConfig],
[
getConversation,
getDefaultConversation,
modularChat,
newConversation,
endpointsConfig,
routeChatProjectId,
],
);
const onSelectPreset = useCallback(
@ -270,7 +289,7 @@ export default function useSelectMention({
logger.info('conversation', 'Switching conversation to new preset', template);
newConversation({
template: { chatProjectId: conversation?.chatProjectId ?? null },
template: { chatProjectId: routeChatProjectId },
preset: newPreset,
keepAddedConvos: isModular,
disableParams,
@ -283,6 +302,7 @@ export default function useSelectMention({
newConversation,
endpointsConfig,
getDefaultConversation,
routeChatProjectId,
],
);

View file

@ -167,6 +167,18 @@ function conversationMatchesProjectQuery(
return conversation.chatProjectId === projectId;
}
/**
* Reads the project id from the current URL's `?projectId` param the source of
* truth for a new chat's project scope (the conversation atom can lag behind it).
*/
export function getRouteChatProjectId(): string | null {
if (typeof window === 'undefined') {
return null;
}
const projectId = new URLSearchParams(window.location.search).get('projectId');
return projectId != null && /^[a-f\d]{24}$/i.test(projectId) ? projectId : null;
}
// === InfiniteData helpers for cursor-based convo queries ===
export function findConversationInInfinite(