fix: project new-chat keeps model spec; sidebar header + row polish

- newConversation: ignore a chatProjectId-only template when deciding to
  apply the default model spec, so starting a chat in a project no longer
  strips the conversation `spec`
- useSelectMention: the Model Selector and @ command now retain the active
  project across endpoint/spec/preset switches; other new-chat paths still
  clear it
- Chats header now matches the Projects header (inline chevron + a new-chat
  icon button) and starts a non-project chat
- Project rows: use the new-chat icon for the per-project add button, render
  at text-sm to match the chat list, and align the row actions + hover color
  with conversation rows
This commit is contained in:
Danny Avila 2026-06-03 09:06:19 -04:00
parent 72783348f4
commit e1e1ebb825
4 changed files with 69 additions and 20 deletions

View file

@ -2,13 +2,21 @@ import { useMemo, memo, type FC, useCallback, useEffect, useRef } from 'react';
import throttle from 'lodash/throttle';
import { ChevronDown } from 'lucide-react';
import { useRecoilValue } from 'recoil';
import { Spinner, useMediaQuery } from '@librechat/client';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys } from 'librechat-data-provider';
import { Spinner, TooltipAnchor, NewChatIcon, useMediaQuery } from '@librechat/client';
import { List, AutoSizer, CellMeasurer, CellMeasurerCache } from 'react-virtualized';
import type { TConversation } from 'librechat-data-provider';
import { useLocalize, TranslationKeys, useFavorites, useShowMarketplace } from '~/hooks';
import {
useLocalize,
TranslationKeys,
useFavorites,
useShowMarketplace,
useNewConvo,
} from '~/hooks';
import FavoritesList from '~/components/Nav/Favorites/FavoritesList';
import { useActiveJobs } from '~/data-provider';
import { groupConversationsByDate, cn } from '~/utils';
import { groupConversationsByDate, clearMessagesCache, cn } from '~/utils';
import Convo from './Convo';
import store from '~/store';
@ -77,20 +85,53 @@ interface ChatsHeaderProps {
onToggle: () => void;
}
const headerIconButtonClassName =
'flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-text-secondary outline-none transition-colors hover:bg-surface-active-alt hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-black dark:focus-visible:ring-white';
/** Collapsible header for the Chats section */
const ChatsHeader: FC<ChatsHeaderProps> = memo(({ isExpanded, onToggle }) => {
const localize = useLocalize();
const queryClient = useQueryClient();
const { newConversation } = useNewConvo();
const conversation = useRecoilValue(store.conversationByIndex(0));
const handleNewChat = useCallback(() => {
clearMessagesCache(queryClient, conversation?.conversationId);
queryClient.invalidateQueries([QueryKeys.messages]);
newConversation();
}, [conversation?.conversationId, newConversation, queryClient]);
return (
<button
onClick={onToggle}
className="group flex w-full items-center justify-between 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"
type="button"
>
<span className="select-none">{localize('com_ui_chats')}</span>
<ChevronDown
className={cn('h-3 w-3 transition-transform duration-200', isExpanded ? 'rotate-180' : '')}
<div className="flex h-8 w-full items-center gap-0.5">
<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"
type="button"
aria-expanded={isExpanded}
>
<span className="select-none truncate">{localize('com_ui_chats')}</span>
<ChevronDown
className={cn(
'h-3 w-3 shrink-0 transition-transform duration-200',
isExpanded ? '' : '-rotate-90',
)}
aria-hidden="true"
/>
</button>
<TooltipAnchor
description={localize('com_ui_new_chat')}
render={
<button
type="button"
aria-label={localize('com_ui_new_chat')}
className={headerIconButtonClassName}
onClick={handleNewChat}
>
<NewChatIcon className="h-4 w-4" />
</button>
}
/>
</button>
</div>
);
});

View file

@ -11,7 +11,6 @@ import {
FolderPlus,
LayoutGrid,
Pencil,
Plus,
Trash2,
} from 'lucide-react';
import { QueryKeys } from 'librechat-data-provider';
@ -27,6 +26,7 @@ import {
OGDialogContent,
TooltipAnchor,
DropdownPopup,
NewChatIcon,
useToastContext,
} from '@librechat/client';
import type { MenuItemProps } from '~/common';
@ -308,7 +308,7 @@ function ProjectItem({
return (
<li className="list-none">
<div className="group/project-row relative flex items-center rounded-lg text-sm text-text-primary transition-colors hover:bg-surface-hover">
<div className="group/project-row relative flex h-9 items-center rounded-lg text-sm text-text-primary transition-colors hover:bg-surface-active-alt">
<button
type="button"
onClick={() => setExpanded((prev) => !prev)}
@ -326,7 +326,7 @@ function ProjectItem({
<Folder className="h-4 w-4 shrink-0 text-text-secondary" aria-hidden="true" />
<span className="truncate">{project.name}</span>
</button>
<div className="absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-0.5 rounded-md bg-surface-hover opacity-0 transition-opacity group-focus-within/project-row:opacity-100 group-hover/project-row:opacity-100 has-[[data-state=open]]:opacity-100">
<div className="absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-0.5 rounded-md bg-surface-active-alt opacity-0 transition-opacity group-focus-within/project-row:opacity-100 group-hover/project-row:opacity-100 has-[[data-state=open]]:opacity-100">
<TooltipAnchor
description={localize('com_ui_new_chat_in_project', { name: project.name })}
render={
@ -336,7 +336,7 @@ function ProjectItem({
className={iconButtonClassName}
onClick={startChat}
>
<Plus className="h-4 w-4" aria-hidden="true" />
<NewChatIcon className="h-4 w-4" />
</button>
}
/>
@ -451,7 +451,7 @@ const ProjectsSection = ({ toggleNav, isAuthenticated }: ProjectsSectionProps) =
}
return (
<div className="flex flex-col px-3">
<div className="flex flex-col px-3 text-sm">
<div className="flex h-8 w-full items-center gap-0.5">
<button
onClick={() => setIsExpanded(!isExpanded)}

View file

@ -107,7 +107,10 @@ export default function useSelectMention({
logger.info('conversation', 'Switching conversation to new spec', conversation);
newConversation({
template: { ...(template as Partial<TConversation>) },
template: {
...(template as Partial<TConversation>),
chatProjectId: conversation?.chatProjectId ?? null,
},
preset,
keepAddedConvos: isModular,
});
@ -201,7 +204,10 @@ export default function useSelectMention({
logger.info('conversation', 'Switching conversation to new endpoint/model', template);
newConversation({
template: { ...(template as Partial<TConversation>) },
template: {
...(template as Partial<TConversation>),
chatProjectId: conversation?.chatProjectId ?? null,
},
preset: { ...kwargs, spec: null, iconURL: null, modelLabel: null, endpoint: newEndpoint },
keepAddedConvos: isNewModular,
});
@ -264,6 +270,7 @@ export default function useSelectMention({
logger.info('conversation', 'Switching conversation to new preset', template);
newConversation({
template: { chatProjectId: conversation?.chatProjectId ?? null },
preset: newPreset,
keepAddedConvos: isModular,
disableParams,

View file

@ -327,7 +327,8 @@ const useNewConvo = (index = 0) => {
startupConfig &&
(startupConfig.modelSpecs?.prioritize === true ||
(startupConfig.interface?.modelSelect ?? true) !== true ||
(result?.last != null && Object.keys(_template).length === 0)) &&
(result?.last != null &&
Object.keys(_template).filter((key) => key !== 'chatProjectId').length === 0)) &&
defaultModelSpec
) {
preset = getModelSpecPreset(defaultModelSpec);