fix: agents show ChatNavbar in both LeftNavbar and TopNavbar layouts (#10718)

* fix: show ChatNavbar in both LeftNavbar and TopNavbar layouts

* Revert "fix: show ChatNavbar in both LeftNavbar and TopNavbar layouts"

This reverts commit 7f205bf241.

* refactor: extract ChatNavBarContent from ChatNavBar

* fix: add navbar content to top nav in left nav mode

* fix: add nodrag to navbar container

* fix: lint error

* fix: ChatNavbarContainer layout

* fix: adjust NavbarLeftContainer min-width for macOS compatibility

---------

Co-authored-by: kangfenmao <kangfenmao@qq.com>
This commit is contained in:
defi-failure 2025-10-17 10:19:48 +08:00 committed by GitHub
parent a290ee7f39
commit 1c2ce7e0aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 186 additions and 136 deletions

View File

@ -74,7 +74,7 @@ const NavbarContainer = styled.div<{ $isFullScreen: boolean }>`
`
const NavbarLeftContainer = styled.div`
min-width: var(--assistants-width);
min-width: ${isMac ? 'calc(var(--assistants-width) - 20px)' : 'var(--assistants-width)'};
padding: 0 10px;
display: flex;
flex-direction: row;

View File

@ -1,34 +1,24 @@
import { BreadcrumbItem, Breadcrumbs, Chip, cn } from '@heroui/react'
import { NavbarHeader } from '@renderer/components/app/Navbar'
import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
import { HStack } from '@renderer/components/Layout'
import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { permissionModeCards } from '@renderer/constants/permissionModes'
import { useAgent } from '@renderer/hooks/agents/useAgent'
import { useSession } from '@renderer/hooks/agents/useSession'
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
import { modelGenerating } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch } from '@renderer/store'
import { setNarrowMode } from '@renderer/store/settings'
import { ApiModel, Assistant, PermissionMode, Topic } from '@renderer/types'
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
import { Assistant, Topic } from '@renderer/types'
import { Tooltip } from 'antd'
import { t } from 'i18next'
import { Menu, PanelLeftClose, PanelRightClose, Search } from 'lucide-react'
import { AnimatePresence, motion } from 'motion/react'
import React, { FC, ReactNode, useCallback } from 'react'
import { FC } from 'react'
import styled from 'styled-components'
import { AgentSettingsPopup } from '../settings/AgentSettings'
import { AgentLabel } from '../settings/AgentSettings/shared'
import AssistantsDrawer from './components/AssistantsDrawer'
import SelectAgentModelButton from './components/SelectAgentModelButton'
import SelectModelButton from './components/SelectModelButton'
import ChatNavbarContent from './components/ChatNavbarContent'
import UpdateAppButton from './components/UpdateAppButton'
interface Props {
@ -45,11 +35,6 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
const { topicPosition, narrowMode } = useSettings()
const { showTopics, toggleShowTopics } = useShowTopics()
const dispatch = useAppDispatch()
const { chat } = useRuntime()
const { activeTopicOrSession, activeAgentId } = chat
const sessionId = activeAgentId ? (chat.activeSessionId[activeAgentId] ?? null) : null
const { agent } = useAgent(activeAgentId)
const { updateModel } = useUpdateAgent()
useShortcut('toggle_show_assistants', toggleShowAssistants)
@ -79,14 +64,6 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
})
}
const handleUpdateModel = useCallback(
async (model: ApiModel) => {
if (!agent) return
return updateModel(agent.id, model.id, { showSuccessToast: false })
},
[agent, updateModel]
)
return (
<NavbarHeader className="home-navbar">
<div className="flex min-w-0 flex-1 shrink items-center overflow-auto">
@ -117,38 +94,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
</motion.div>
)}
</AnimatePresence>
{activeTopicOrSession === 'topic' && <SelectModelButton assistant={assistant} />}
{activeTopicOrSession === 'session' && agent && (
<HorizontalScrollContainer>
<Breadcrumbs
classNames={{
base: 'flex',
list: 'flex-nowrap'
}}>
<BreadcrumbItem
onPress={() => AgentSettingsPopup.show({ agentId: agent.id })}
classNames={{
base: 'self-stretch',
item: 'h-full'
}}>
<Chip size="md" variant="light" className="h-full transition-background hover:bg-foreground-100">
<AgentLabel
agent={agent}
classNames={{ name: 'max-w-50 font-bold text-xs', avatar: 'h-4.5 w-4.5', container: 'gap-1.5' }}
/>
</Chip>
</BreadcrumbItem>
<BreadcrumbItem>
<SelectAgentModelButton agent={agent} onSelect={handleUpdateModel} />
</BreadcrumbItem>
{activeAgentId && sessionId && (
<BreadcrumbItem>
<SessionWorkspaceMeta agentId={activeAgentId} sessionId={sessionId} />
</BreadcrumbItem>
)}
</Breadcrumbs>
</HorizontalScrollContainer>
)}
<ChatNavbarContent assistant={assistant} />
</div>
<HStack alignItems="center" gap={8}>
<UpdateAppButton />
@ -181,74 +127,6 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
)
}
const SessionWorkspaceMeta: FC<{ agentId: string; sessionId: string }> = ({ agentId, sessionId }) => {
const { agent } = useAgent(agentId)
const { session } = useSession(agentId, sessionId)
if (!session || !agent) {
return null
}
const firstAccessiblePath = session.accessible_paths?.[0]
const permissionMode = (session.configuration?.permission_mode ?? 'default') as PermissionMode
const permissionModeCard = permissionModeCards.find((card) => card.mode === permissionMode)
const permissionModeLabel = permissionModeCard
? t(permissionModeCard.titleKey, permissionModeCard.titleFallback)
: permissionMode
const infoItems: ReactNode[] = []
const InfoTag = ({
text,
className,
onClick
}: {
text: string
className?: string
classNames?: {}
onClick?: (e: React.MouseEvent) => void
}) => (
<div
className={cn(
'rounded-medium border border-default-200 px-2 py-1 text-foreground-500 text-xs dark:text-foreground-400',
onClick !== undefined ? 'cursor-pointer' : undefined,
className
)}
title={text}
onClick={onClick}>
<span className="block truncate">{text}</span>
</div>
)
// infoItems.push(<InfoTag key="name" text={agent.name ?? ''} className="max-w-60" />)
if (firstAccessiblePath) {
infoItems.push(
<InfoTag
key="path"
text={firstAccessiblePath}
className="max-w-60 transition-colors hover:border-primary hover:text-primary"
onClick={() => {
window.api.file
.openPath(firstAccessiblePath)
.catch((e) =>
window.toast.error(
formatErrorMessageWithPrefix(e, t('files.error.open_path', { path: firstAccessiblePath }))
)
)
}}
/>
)
}
infoItems.push(<InfoTag key="permission-mode" text={permissionModeLabel} className="max-w-50" />)
if (infoItems.length === 0) {
return null
}
return <div className="ml-2 flex items-center gap-2">{infoItems}</div>
}
export const NavbarIcon = styled.div`
-webkit-app-region: none;
border-radius: 8px;

View File

@ -130,6 +130,7 @@ const HomePage: FC = () => {
setActiveTopic={setActiveTopic}
setActiveAssistant={setActiveAssistant}
position="left"
activeTopicOrSession={activeTopicOrSession}
/>
)}
<ContentContainer id={isLeftNavbar ? 'content-container' : undefined}>

View File

@ -1,4 +1,4 @@
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import { Navbar, NavbarCenter, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout'
import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { isLinux, isMac, isWin } from '@renderer/config/constant'
@ -7,6 +7,8 @@ import { modelGenerating } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import { useChatMaxWidth } from '@renderer/pages/home/Chat'
import ChatNavbarContent from '@renderer/pages/home/components/ChatNavbarContent'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch } from '@renderer/store'
import { setNarrowMode } from '@renderer/store/settings'
@ -15,7 +17,7 @@ import { Tooltip } from 'antd'
import { t } from 'i18next'
import { Menu, PanelLeftClose, PanelRightClose, Search } from 'lucide-react'
import { AnimatePresence, motion } from 'motion/react'
import { FC } from 'react'
import React, { FC } from 'react'
import styled from 'styled-components'
import AssistantsDrawer from './components/AssistantsDrawer'
@ -28,13 +30,21 @@ interface Props {
setActiveTopic: (topic: Topic) => void
setActiveAssistant: (assistant: Assistant) => void
position: 'left' | 'right'
activeTopicOrSession?: 'topic' | 'session'
}
const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTopic, setActiveTopic }) => {
const HeaderNavbar: FC<Props> = ({
activeAssistant,
setActiveAssistant,
activeTopic,
setActiveTopic,
activeTopicOrSession
}) => {
const { assistant } = useAssistant(activeAssistant.id)
const { showAssistants, toggleShowAssistants } = useShowAssistants()
const { topicPosition, narrowMode } = useSettings()
const { showTopics, toggleShowTopics } = useShowTopics()
const chatMaxWidth = useChatMaxWidth()
const dispatch = useAppDispatch()
useShortcut('toggle_show_assistants', toggleShowAssistants)
@ -113,15 +123,29 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
</AnimatePresence>
</NavbarLeft>
)}
<HStack alignItems="center" gap={6} ml={!isMac ? 16 : 0}>
<SelectModelButton assistant={assistant} />
</HStack>
<NavbarCenter>
{activeTopicOrSession === 'topic' ? (
<HStack alignItems="center" gap={6} ml={!isMac ? 16 : 0}>
<SelectModelButton assistant={assistant} />
</HStack>
) : (
<ChatNavbarContainer
style={{
maxWidth: chatMaxWidth,
marginLeft: !isMac ? 16 : 0
}}>
<ChatNavbarContent assistant={assistant} />
</ChatNavbarContainer>
)}
</NavbarCenter>
<NavbarRight
style={{
justifyContent: 'flex-end',
flex: 1,
flex: activeTopicOrSession === 'topic' ? 1 : 'none',
position: 'relative',
paddingRight: isWin || isLinux ? '144px' : '15px'
paddingRight: isWin || isLinux ? '144px' : '15px',
minWidth: activeTopicOrSession === 'topic' ? '' : 'auto'
}}
className="home-navbar-right">
<HStack alignItems="center" gap={6}>
@ -196,4 +220,15 @@ const NarrowIcon = styled(NavbarIcon)`
}
`
const ChatNavbarContainer: React.FC<{ children: React.ReactNode; style?: React.CSSProperties }> = ({
children,
style
}) => {
return (
<div className="nodrag flex min-w-0 flex-1 items-center justify-start gap-1.5 overflow-hidden" style={style}>
{children}
</div>
)
}
export default HeaderNavbar

View File

@ -0,0 +1,136 @@
import { BreadcrumbItem, Breadcrumbs, Chip, cn } from '@heroui/react'
import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
import { permissionModeCards } from '@renderer/constants/permissionModes'
import { useAgent } from '@renderer/hooks/agents/useAgent'
import { useSession } from '@renderer/hooks/agents/useSession'
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { ApiModel, Assistant, PermissionMode } from '@renderer/types'
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
import { t } from 'i18next'
import { FC, ReactNode, useCallback } from 'react'
import { AgentSettingsPopup } from '../../settings/AgentSettings'
import { AgentLabel } from '../../settings/AgentSettings/shared'
import SelectAgentModelButton from './SelectAgentModelButton'
import SelectModelButton from './SelectModelButton'
interface Props {
assistant: Assistant
}
const ChatNavbarContent: FC<Props> = ({ assistant }) => {
const { chat } = useRuntime()
const { activeTopicOrSession, activeAgentId } = chat
const sessionId = activeAgentId ? (chat.activeSessionId[activeAgentId] ?? null) : null
const { agent } = useAgent(activeAgentId)
const { updateModel } = useUpdateAgent()
const handleUpdateModel = useCallback(
async (model: ApiModel) => {
if (!agent) return
return updateModel(agent.id, model.id, { showSuccessToast: false })
},
[agent, updateModel]
)
return (
<>
{activeTopicOrSession === 'topic' && <SelectModelButton assistant={assistant} />}
{activeTopicOrSession === 'session' && agent && (
<HorizontalScrollContainer>
<Breadcrumbs classNames={{ base: 'flex', list: 'flex-nowrap' }}>
<BreadcrumbItem
onPress={() => AgentSettingsPopup.show({ agentId: agent.id })}
classNames={{ base: 'self-stretch', item: 'h-full' }}>
<Chip size="md" variant="light" className="h-full transition-background hover:bg-foreground-100">
<AgentLabel
agent={agent}
classNames={{ name: 'max-w-50 font-bold text-xs', avatar: 'h-2 w-2 ml-[-4px]', container: 'gap-1.5' }}
/>
</Chip>
</BreadcrumbItem>
<BreadcrumbItem>
<SelectAgentModelButton agent={agent} onSelect={handleUpdateModel} />
</BreadcrumbItem>
{activeAgentId && sessionId && (
<BreadcrumbItem>
<SessionWorkspaceMeta agentId={activeAgentId} sessionId={sessionId} />
</BreadcrumbItem>
)}
</Breadcrumbs>
</HorizontalScrollContainer>
)}
</>
)
}
const SessionWorkspaceMeta: FC<{ agentId: string; sessionId: string }> = ({ agentId, sessionId }) => {
const { agent } = useAgent(agentId)
const { session } = useSession(agentId, sessionId)
if (!session || !agent) {
return null
}
const firstAccessiblePath = session.accessible_paths?.[0]
const permissionMode = (session.configuration?.permission_mode ?? 'default') as PermissionMode
const permissionModeCard = permissionModeCards.find((card) => card.mode === permissionMode)
const permissionModeLabel = permissionModeCard
? t(permissionModeCard.titleKey, permissionModeCard.titleFallback)
: permissionMode
const infoItems: ReactNode[] = []
const InfoTag = ({
text,
className,
onClick
}: {
text: string
className?: string
classNames?: {}
onClick?: (e: React.MouseEvent) => void
}) => (
<div
className={cn(
'rounded-medium border border-default-200 px-2 py-1 text-foreground-500 text-xs dark:text-foreground-400',
onClick !== undefined ? 'cursor-pointer' : undefined,
className
)}
title={text}
onClick={onClick}>
<span className="block truncate">{text}</span>
</div>
)
// infoItems.push(<InfoTag key="name" text={agent.name ?? ''} className="max-w-60" />)
if (firstAccessiblePath) {
infoItems.push(
<InfoTag
key="path"
text={firstAccessiblePath}
className="max-w-60 transition-colors hover:border-primary hover:text-primary"
onClick={() => {
window.api.file
.openPath(firstAccessiblePath)
.catch((e) =>
window.toast.error(
formatErrorMessageWithPrefix(e, t('files.error.open_path', { path: firstAccessiblePath }))
)
)
}}
/>
)
}
infoItems.push(<InfoTag key="permission-mode" text={permissionModeLabel} className="max-w-50" />)
if (infoItems.length === 0) {
return null
}
return <div className="ml-2 flex items-center gap-2">{infoItems}</div>
}
export default ChatNavbarContent