feat(sessions): implement session management ui and state

- Rename Container to ButtonContainer for consistency
- Add activeSessionId state to track active sessions per agent
- Implement Sessions and SessionItem components with loading state
- Add session selection and deletion functionality
This commit is contained in:
icarus 2025-09-19 14:54:04 +08:00
parent b3ef6d4534
commit f127150ea1
4 changed files with 62 additions and 29 deletions

View File

@ -36,11 +36,11 @@ const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete, onPress }) =
<>
<ContextMenu modal={false}>
<ContextMenuTrigger>
<Container onPress={onPress} className={isActive ? 'active' : ''}>
<ButtonContainer onPress={onPress} className={isActive ? 'active' : ''}>
<AssistantNameRow className="name" title={agent.name ?? agent.id}>
<AgentLabel />
</AssistantNameRow>
</Container>
</ButtonContainer>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
@ -73,7 +73,7 @@ const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete, onPress }) =
)
}
const Container: React.FC<React.ComponentProps<typeof Button>> = ({ className, children, ...props }) => (
const ButtonContainer: React.FC<React.ComponentProps<typeof Button>> = ({ className, children, ...props }) => (
<Button
{...props}
className={cn(

View File

@ -1,45 +1,48 @@
import { Button, cn, useDisclosure } from '@heroui/react'
import { loggerService } from '@logger'
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { AgentSessionEntity } from '@renderer/types'
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@renderer/ui/context-menu'
import { FC, memo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
const logger = loggerService.withContext('AgentItem')
// const logger = loggerService.withContext('AgentItem')
interface SessionItemProps {
session: AgentSessionEntity
isActive: boolean
onDelete: (session: AgentSessionEntity) => void
// use external agentId as SSOT, instead of session.agent_id
agentId: string
onDelete: () => void
onPress: () => void
}
const SessionItem: FC<SessionItemProps> = ({ session, isActive, onDelete, onPress }) => {
const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress }) => {
const { t } = useTranslation()
// const { isOpen, onOpen, onClose } = useDisclosure()
const { onOpen } = useDisclosure()
const { chat } = useRuntime()
const activeSessionId = chat.activeSessionId[agentId]
const isActive = activeSessionId === session.id
const SessionLabel = useCallback(() => {
const displayName = session.name ?? session.id
return (
<Button onPress={onPress}>
<Button>
<span className="text-sm">{displayName}</span>
</Button>
)
}, [session.id, session.name, onPress])
const handleClick = () => logger.debug('not implemented')
}, [session.id, session.name])
return (
<>
<ContextMenu modal={false}>
<ContextMenuTrigger>
<Container onClick={handleClick} className={isActive ? 'active' : ''}>
<ButtonContainer onPress={onPress} className={isActive ? 'active' : ''}>
<SessionLabelContainer className="name" title={session.name ?? session.id}>
<SessionLabel />
</SessionLabelContainer>
</Container>
</ButtonContainer>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
@ -59,7 +62,7 @@ const SessionItem: FC<SessionItemProps> = ({ session, isActive, onDelete, onPres
content: t('agent.session.delete.content'),
centered: true,
okButtonProps: { danger: true },
onOk: () => onDelete(session)
onOk: () => onDelete()
})
}}>
<DeleteIcon size={14} className="lucide-custom text-danger" />
@ -72,20 +75,21 @@ const SessionItem: FC<SessionItemProps> = ({ session, isActive, onDelete, onPres
)
}
const Container: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
<div
const ButtonContainer: React.FC<React.ComponentProps<typeof Button>> = ({ className, children, ...props }) => (
<Button
{...props}
className={cn(
'relative flex h-[37px] flex-row justify-between p-2',
'relative mb-2 flex h-[37px] flex-row justify-between p-2.5',
'rounded-[var(--list-item-border-radius)]',
'border-[0.5px] border-transparent',
'w-[calc(var(--assistants-width)_-_20px)]',
'hover:bg-[var(--color-list-item-hover)]',
'bg-transparent hover:bg-[var(--color-list-item-hover)]',
'cursor-pointer',
className?.includes('active') && 'bg-[var(--color-list-item)] shadow-sm',
className
)}
/>
)}>
{children}
</Button>
)
const SessionLabelContainer: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (

View File

@ -1,23 +1,43 @@
import { loggerService } from '@logger'
import { Spinner } from '@heroui/react'
import { useSessions } from '@renderer/hooks/agents/useSessions'
import { memo } from 'react'
import { useAppDispatch } from '@renderer/store'
import { setActiveSessionIdAction } from '@renderer/store/runtime'
import { memo, useCallback } from 'react'
const logger = loggerService.withContext('SessionsTab')
import SessionItem from './SessionItem'
// const logger = loggerService.withContext('SessionsTab')
interface SessionsProps {
agentId: string
}
const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
const { sessions } = useSessions(agentId)
logger.debug('Sessions', sessions)
const { sessions, isLoading, deleteSession } = useSessions(agentId)
const dispatch = useAppDispatch()
const setActiveSessionId = useCallback(
(agentId: string, sessionId: string | null) => {
dispatch(setActiveSessionIdAction({ agentId, sessionId }))
},
[dispatch]
)
if (isLoading) return <Spinner />
// if (error) return
return (
<div className="agents-tab h-full w-full p-2">
{/* TODO: Add session button */}
Active Agent ID: {agentId}
{sessions.map((session) => (
<div key={session.id}>Not implemented</div>
<SessionItem
key={session.id}
session={session}
agentId={agentId}
onDelete={() => deleteSession(session.id)}
onPress={() => setActiveSessionId(agentId, session.id)}
/>
))}
</div>
)

View File

@ -7,8 +7,11 @@ export interface ChatState {
isMultiSelectMode: boolean
selectedMessageIds: string[]
activeTopic: Topic | null
/** UI state. null represents no active agent, may active assistant */
/** UI state. null represents no active agent */
activeAgentId: string | null
/** UI state. Map agent id to active session id.
* null represents no active session */
activeSessionId: Record<string, string | null>
/** topic ids that are currently being renamed */
renamingTopics: string[]
/** topic ids that are newly renamed */
@ -81,6 +84,7 @@ const initialState: RuntimeState = {
selectedMessageIds: [],
activeTopic: null,
activeAgentId: null,
activeSessionId: {},
renamingTopics: [],
newlyRenamedTopics: []
},
@ -148,6 +152,10 @@ const runtimeSlice = createSlice({
setActiveAgentId: (state, action: PayloadAction<string>) => {
state.chat.activeAgentId = action.payload
},
setActiveSessionIdAction: (state, action: PayloadAction<{ agentId: string; sessionId: string | null }>) => {
const { agentId, sessionId } = action.payload
state.chat.activeSessionId[agentId] = sessionId
},
setRenamingTopics: (state, action: PayloadAction<string[]>) => {
state.chat.renamingTopics = action.payload
},
@ -187,6 +195,7 @@ export const {
setSelectedMessageIds,
setActiveTopic,
setActiveAgentId,
setActiveSessionIdAction,
setRenamingTopics,
setNewlyRenamedTopics,
// WebSearch related actions