feat: session settings (#10773)

* fix(home/Tabs): remove redundant isTopicView check in tab rendering

* refactor(runtime): rename activeSessionId to activeSessionIdMap for clarity

Update variable name to better reflect its purpose as a mapping structure

* refactor(agent): add CreateAgentSessionResponse type and schema

Add new type and schema for create session response to better reflect API contract

* fix(useSessions): return null instead of undefined on session creation error

Returning null provides better type safety and aligns with the response type Promise<CreateAgentSessionResponse | null>

* refactor(useSessions): add return type to deleteSession callback

* fix(useSessions): return session data or null from getSession

Ensure getSession callback always returns a value (session data or null) to handle error cases properly and improve type safety

* feat(hooks): add useActiveSession hook and handle null agentId in useSessions

Add new hook to get active session from runtime and sessions data. Update useSessions to handle null agentId cases by returning early and adding null checks.

* feat(hooks): add useActiveAgent hook to get active agent

Expose active agent data by combining useRuntime and useAgent hooks

* fix(agents): remove fake agent id handling and improve null checks

- Replace fake agent id with null in HomePage
- Remove fake id check in useAgent hook and throw error for null id
- Simplify agent session initialization by removing fake id checks

* refactor(hooks): replace useAgent with useActiveAgent for active agent state

* feat(home): add session settings tab component

Replace AgentSettingsTab with SessionSettingsTab to better handle session-specific settings. The new component includes essential and advanced settings sections with a more settings button.

* refactor(settings): consolidate agent and session essential settings into single component

Replace AgentEssentialSettings and SessionEssentialSettings with a unified EssentialSettings component that handles both agent and session types. This reduces code duplication and improves maintainability.

* style(SelectAgentModelButton): improve model name display with truncation

Add overflow-x-hidden to container and truncate to model name span to prevent text overflow

* refactor(AgentSettings): replace Ellipsis with truncate for text overflow

Use CSS truncate instead of Ellipsis component for better performance and consistency

* refactor(chat-navbar): replace useAgent and useSession with useActiveAgent and useActiveSession

Simplify component logic by using dedicated hooks for active agent and session

* feat(ChatNavbar): add session settings button to breadcrumb

Add clickable session label chip that opens session settings popup when active session exists

* refactor(agents): improve session update hook and type definitions

- Extract UpdateAgentBaseOptions type to shared types file
- Update useUpdateSession to return both updateSession and updateModel functions
- Modify components to use destructured updateSession from hook
- Add null check for agentId in useUpdateSession
- Add success toast option to session updates

* refactor(components): rename agent prop to agentBase for clarity

Update component name and prop to better reflect its purpose and improve code readability

* refactor(ChatNavbar): rename SelectAgentModelButton to SelectAgentBaseModelButton and update usage

Update component name to better reflect its purpose and adjust props to use activeSession instead of activeAgent for consistency

* feat(i18n): add null id error message for agent retrieval

Add error message for when agent ID is null across all supported languages

* refactor(hooks): simplify agent and session hooks by returning destructured values

Remove unnecessary intermediate variables and directly return hook results
Update useSession to handle null agentId and sessionId cases

* feat(i18n): add null session ID error message for all locales

* refactor(home): rename SelectAgentModelButton to SelectAgentBaseModelButton

The component was renamed to better reflect its purpose of selecting base models for agents. The functionality remains unchanged.

* refactor(session): rename useUpdateAgent to useUpdateSession for clarity

* refactor(home-tabs): replace useUpdateAgent with useUpdateSession hook

Update session settings tab to use the new useUpdateSession hook which requires activeAgentId

* style(AgentSettings): remove unnecessary gap class from ModelSetting

* refactor(agents): improve error handling and remove duplicate code

- Replace formatErrorMessageWithPrefix with getErrorMessage for better error handling
- Move updateSession logic to useUpdateSession hook to avoid duplication

* fix(ChatNavbar): prevent model update when activeAgent is missing

Add activeAgent to dependency array and check its existence before updating model to avoid potential errors

* feat(home/Tabs): add loading and error states for session settings

Add Skeleton loader and Alert component to handle loading and error states when fetching session data in the settings tab

* fix(home/Tabs): add h-full class to Skeleton for proper height

* fix(AssistantsTab): remove weird effect hook for agent selection

* refactor(chat-navbar): clean up unused code and update session handling

remove commented out code in ChatNavbar.tsx and update ChatNavbarContent to use active agent/session hooks

* style(home): remove negative margin from model name span

* refactor(Agents): mark Agents component as deprecated

* refactor: remove unused Agents and Assistants code

---------

Co-authored-by: dev <verc20.dev@proton.me>
This commit is contained in:
Phantom 2025-10-17 19:44:47 +08:00 committed by GitHub
parent d1a9dfa3e6
commit 4eb3aa31ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 348 additions and 555 deletions

View File

@ -9,6 +9,8 @@ import {
CreateAgentRequest,
CreateAgentResponse,
CreateAgentResponseSchema,
CreateAgentSessionResponse,
CreateAgentSessionResponseSchema,
CreateSessionForm,
CreateSessionRequest,
GetAgentResponse,
@ -171,12 +173,12 @@ export class AgentApiClient {
}
}
public async createSession(agentId: string, session: CreateSessionForm): Promise<GetAgentSessionResponse> {
public async createSession(agentId: string, session: CreateSessionForm): Promise<CreateAgentSessionResponse> {
const url = this.getSessionPaths(agentId).base
try {
const payload = session satisfies CreateSessionRequest
const response = await this.axios.post(url, payload)
const data = GetAgentSessionResponseSchema.parse(response.data)
const data = CreateAgentSessionResponseSchema.parse(response.data)
return data
} catch (error) {
throw processError(error, 'Failed to add session.')

View File

@ -98,7 +98,7 @@ export const SessionModal: React.FC<Props> = ({
const loadingRef = useRef(false)
// const { setTimeoutTimer } = useTimer()
const { createSession } = useSessions(agentId)
const updateSession = useUpdateSession(agentId)
const { updateSession } = useUpdateSession(agentId)
const { agent } = useAgent(agentId)
const isEditing = (session?: AgentSessionEntity) => session !== undefined

View File

@ -0,0 +1,4 @@
export type UpdateAgentBaseOptions = {
/** Whether to show success toast after updating. Defaults to true. */
showSuccessToast?: boolean
}

View File

@ -0,0 +1,8 @@
import { useRuntime } from '../useRuntime'
import { useAgent } from './useAgent'
export const useActiveAgent = () => {
const { chat } = useRuntime()
const { activeAgentId } = chat
return useAgent(activeAgentId)
}

View File

@ -0,0 +1,9 @@
import { useRuntime } from '../useRuntime'
import { useSession } from './useSession'
export const useActiveSession = () => {
const { chat } = useRuntime()
const { activeSessionIdMap, activeAgentId } = chat
const activeSessionId = activeAgentId ? activeSessionIdMap[activeAgentId] : null
return useSession(activeAgentId, activeSessionId)
}

View File

@ -11,8 +11,8 @@ export const useAgent = (id: string | null) => {
const key = id ? client.agentPaths.withId(id) : null
const { apiServerConfig, apiServerRunning } = useApiServer()
const fetcher = useCallback(async () => {
if (!id || id === 'fake') {
return null
if (!id) {
throw new Error(t('agent.get.error.null_id'))
}
if (!apiServerConfig.enabled) {
throw new Error(t('apiServer.messages.notEnabled'))

View File

@ -17,7 +17,7 @@ export const useAgentSessionInitializer = () => {
const dispatch = useAppDispatch()
const client = useAgentClient()
const { chat } = useRuntime()
const { activeAgentId, activeSessionId } = chat
const { activeAgentId, activeSessionIdMap } = chat
/**
* Initialize session for the given agent by loading its sessions
@ -25,11 +25,11 @@ export const useAgentSessionInitializer = () => {
*/
const initializeAgentSession = useCallback(
async (agentId: string) => {
if (!agentId || agentId === 'fake') return
if (!agentId) return
try {
// Check if this agent already has an active session
const currentSessionId = activeSessionId[agentId]
const currentSessionId = activeSessionIdMap[agentId]
if (currentSessionId) {
// Session already exists, just switch to session view
dispatch(setActiveTopicOrSessionAction('session'))
@ -58,21 +58,21 @@ export const useAgentSessionInitializer = () => {
dispatch(setActiveTopicOrSessionAction('session'))
}
},
[client, dispatch, activeSessionId]
[client, dispatch, activeSessionIdMap]
)
/**
* Auto-initialize when activeAgentId changes
*/
useEffect(() => {
if (activeAgentId && activeAgentId !== 'fake') {
if (activeAgentId) {
// Check if we need to initialize this agent's session
const hasActiveSession = activeSessionId[activeAgentId]
const hasActiveSession = activeSessionIdMap[activeAgentId]
if (!hasActiveSession) {
initializeAgentSession(activeAgentId)
}
}
}, [activeAgentId, activeSessionId, initializeAgentSession])
}, [activeAgentId, activeSessionIdMap, initializeAgentSession])
return {
initializeAgentSession

View File

@ -1,21 +1,24 @@
import { useAppDispatch } from '@renderer/store'
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { UpdateSessionForm } from '@renderer/types'
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
import { useCallback, useEffect, useMemo } from 'react'
import { useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { useAgentClient } from './useAgentClient'
import { useUpdateSession } from './useUpdateSession'
export const useSession = (agentId: string, sessionId: string) => {
export const useSession = (agentId: string | null, sessionId: string | null) => {
const { t } = useTranslation()
const client = useAgentClient()
const key = client.getSessionPaths(agentId).withId(sessionId)
const key = agentId && sessionId ? client.getSessionPaths(agentId).withId(sessionId) : null
const dispatch = useAppDispatch()
const sessionTopicId = useMemo(() => buildAgentSessionTopicId(sessionId), [sessionId])
const sessionTopicId = useMemo(() => (sessionId ? buildAgentSessionTopicId(sessionId) : null), [sessionId])
const { updateSession } = useUpdateSession(agentId)
const fetcher = async () => {
if (!agentId) throw new Error(t('agent.get.error.null_id'))
if (!sessionId) throw new Error(t('agent.session.get.error.null_id'))
const data = await client.getSession(agentId, sessionId)
return data
}
@ -24,26 +27,13 @@ export const useSession = (agentId: string, sessionId: string) => {
// Use loadTopicMessagesThunk to load messages (with caching mechanism)
// This ensures messages are preserved when switching between sessions/tabs
useEffect(() => {
if (sessionId) {
if (sessionTopicId) {
// loadTopicMessagesThunk will check if messages already exist in Redux
// and skip loading if they do (unless forceReload is true)
dispatch(loadTopicMessagesThunk(sessionTopicId))
}
}, [dispatch, sessionId, sessionTopicId])
const updateSession = useCallback(
async (form: UpdateSessionForm) => {
if (!agentId) return
try {
const result = await client.updateSession(agentId, form)
mutate(result)
} catch (error) {
window.toast.error(t('agent.session.update.error.failed'))
}
},
[agentId, client, mutate, t]
)
return {
session: data,
error,

View File

@ -1,4 +1,4 @@
import { CreateSessionForm } from '@renderer/types'
import { CreateAgentSessionResponse, CreateSessionForm, GetAgentSessionResponse } from '@renderer/types'
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
@ -6,46 +6,50 @@ import useSWR from 'swr'
import { useAgentClient } from './useAgentClient'
export const useSessions = (agentId: string) => {
export const useSessions = (agentId: string | null) => {
const { t } = useTranslation()
const client = useAgentClient()
const key = client.getSessionPaths(agentId).base
const key = agentId ? client.getSessionPaths(agentId).base : null
const fetcher = async () => {
if (!agentId) throw new Error('No active agent.')
const data = await client.listSessions(agentId)
return data.data
}
const { data, error, isLoading, mutate } = useSWR(key, fetcher)
const createSession = useCallback(
async (form: CreateSessionForm) => {
async (form: CreateSessionForm): Promise<CreateAgentSessionResponse | null> => {
if (!agentId) return null
try {
const result = await client.createSession(agentId, form)
await mutate((prev) => [result, ...(prev ?? [])], { revalidate: false })
return result
} catch (error) {
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.create.error.failed')))
return undefined
return null
}
},
[agentId, client, mutate, t]
)
// TODO: including messages field
const getSession = useCallback(
async (id: string) => {
async (id: string): Promise<GetAgentSessionResponse | null> => {
if (!agentId) return null
try {
const result = await client.getSession(agentId, id)
mutate((prev) => prev?.map((session) => (session.id === result.id ? result : session)))
return result
} catch (error) {
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.get.error.failed')))
return null
}
},
[agentId, client, mutate, t]
)
const deleteSession = useCallback(
async (id: string) => {
async (id: string): Promise<boolean> => {
if (!agentId) return false
try {
await client.deleteSession(agentId, id)

View File

@ -4,20 +4,16 @@ import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { mutate } from 'swr'
import { UpdateAgentBaseOptions } from './types'
import { useAgentClient } from './useAgentClient'
export type UpdateAgentOptions = {
/** Whether to show success toast after updating. Defaults to true. */
showSuccessToast?: boolean
}
export const useUpdateAgent = () => {
const { t } = useTranslation()
const client = useAgentClient()
const listKey = client.agentPaths.base
const updateAgent = useCallback(
async (form: UpdateAgentForm, options?: UpdateAgentOptions) => {
async (form: UpdateAgentForm, options?: UpdateAgentBaseOptions) => {
try {
const itemKey = client.agentPaths.withId(form.id)
// may change to optimistic update
@ -35,7 +31,7 @@ export const useUpdateAgent = () => {
)
const updateModel = useCallback(
async (agentId: string, modelId: string, options?: UpdateAgentOptions) => {
async (agentId: string, modelId: string, options?: UpdateAgentBaseOptions) => {
updateAgent({ id: agentId, model: modelId }, options)
},
[updateAgent]

View File

@ -1,19 +1,21 @@
import { ListAgentSessionsResponse, UpdateSessionForm } from '@renderer/types'
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
import { getErrorMessage } from '@renderer/utils/error'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { mutate } from 'swr'
import { UpdateAgentBaseOptions } from './types'
import { useAgentClient } from './useAgentClient'
export const useUpdateSession = (agentId: string) => {
export const useUpdateSession = (agentId: string | null) => {
const { t } = useTranslation()
const client = useAgentClient()
const paths = client.getSessionPaths(agentId)
const listKey = paths.base
const updateSession = useCallback(
async (form: UpdateSessionForm) => {
async (form: UpdateSessionForm, options?: UpdateAgentBaseOptions) => {
if (!agentId) return
const paths = client.getSessionPaths(agentId)
const listKey = paths.base
const sessionId = form.id
try {
const itemKey = paths.withId(sessionId)
@ -24,13 +26,29 @@ export const useUpdateSession = (agentId: string) => {
(prev) => prev?.map((session) => (session.id === result.id ? result : session)) ?? []
)
mutate(itemKey, result)
window.toast.success(t('common.update_success'))
if (options?.showSuccessToast ?? true) {
window.toast.success(t('common.update_success'))
}
} catch (error) {
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.update.error.failed')))
window.toast.error({ title: t('agent.session.update.error.failed'), description: getErrorMessage(error) })
}
},
[agentId, client, listKey, paths, t]
[agentId, client, t]
)
return updateSession
const updateModel = useCallback(
async (sessionId: string, modelId: string, options?: UpdateAgentBaseOptions) => {
if (!agentId) return
return updateSession(
{
id: sessionId,
model: modelId
},
options
)
},
[agentId, updateSession]
)
return { updateSession, updateModel }
}

View File

@ -22,7 +22,8 @@
},
"get": {
"error": {
"failed": "Failed to get the agent."
"failed": "Failed to get the agent.",
"null_id": "Agent ID is null."
}
},
"list": {
@ -73,7 +74,8 @@
},
"get": {
"error": {
"failed": "Failed to get the session"
"failed": "Failed to get the session",
"null_id": "Session ID is null"
}
},
"label_one": "Session",

View File

@ -22,7 +22,8 @@
},
"get": {
"error": {
"failed": "获取智能体失败"
"failed": "获取智能体失败",
"null_id": "智能体 ID 为空。"
}
},
"list": {
@ -73,7 +74,8 @@
},
"get": {
"error": {
"failed": "获取会话失败"
"failed": "获取会话失败",
"null_id": "会话 ID 为空"
}
},
"label_one": "会话",

View File

@ -22,7 +22,8 @@
},
"get": {
"error": {
"failed": "無法取得代理程式。"
"failed": "無法取得代理程式。",
"null_id": "代理程式 ID 為空。"
}
},
"list": {
@ -73,7 +74,8 @@
},
"get": {
"error": {
"failed": "無法取得工作階段"
"failed": "無法取得工作階段",
"null_id": "工作階段 ID 為空"
}
},
"label_one": "會議",

View File

@ -22,7 +22,8 @@
},
"get": {
"error": {
"failed": "Αποτυχία λήψης του πράκτορα."
"failed": "Αποτυχία λήψης του πράκτορα.",
"null_id": "Το ID του πράκτορα είναι null."
}
},
"list": {
@ -73,7 +74,8 @@
},
"get": {
"error": {
"failed": "Αποτυχία λήψης της συνεδρίας"
"failed": "Αποτυχία λήψης της συνεδρίας",
"null_id": "Το ID της συνεδρίας είναι null"
}
},
"label_one": "Συνεδρία",

View File

@ -22,7 +22,8 @@
},
"get": {
"error": {
"failed": "No se pudo obtener el agente."
"failed": "No se pudo obtener el agente.",
"null_id": "El ID del agente es nulo."
}
},
"list": {
@ -73,7 +74,8 @@
},
"get": {
"error": {
"failed": "Error al obtener la sesión"
"failed": "Error al obtener la sesión",
"null_id": "El ID de sesión es nulo"
}
},
"label_one": "Sesión",

View File

@ -22,7 +22,8 @@
},
"get": {
"error": {
"failed": "Échec de l'obtention de l'agent."
"failed": "Échec de l'obtention de l'agent.",
"null_id": "L'ID de l'agent est nul."
}
},
"list": {
@ -73,7 +74,8 @@
},
"get": {
"error": {
"failed": "Échec de l'obtention de la session"
"failed": "Échec de l'obtention de la session",
"null_id": "L'ID de session est nul"
}
},
"label_one": "Session",

View File

@ -22,7 +22,8 @@
},
"get": {
"error": {
"failed": "エージェントの取得に失敗しました。"
"failed": "エージェントの取得に失敗しました。",
"null_id": "エージェント ID が null です。"
}
},
"list": {
@ -73,7 +74,8 @@
},
"get": {
"error": {
"failed": "セッションの取得に失敗しました"
"failed": "セッションの取得に失敗しました",
"null_id": "セッション ID が null です"
}
},
"label_one": "セッション",

View File

@ -22,7 +22,8 @@
},
"get": {
"error": {
"failed": "Falha ao obter o agente."
"failed": "Falha ao obter o agente.",
"null_id": "O ID do agente é nulo."
}
},
"list": {
@ -73,7 +74,8 @@
},
"get": {
"error": {
"failed": "Falha ao obter a sessão"
"failed": "Falha ao obter a sessão",
"null_id": "O ID da sessão é nulo"
}
},
"label_one": "Sessão",

View File

@ -22,7 +22,8 @@
},
"get": {
"error": {
"failed": "Не удалось получить агента."
"failed": "Не удалось получить агента.",
"null_id": "ID агента равен null."
}
},
"list": {
@ -73,7 +74,8 @@
},
"get": {
"error": {
"failed": "Не удалось получить сеанс"
"failed": "Не удалось получить сеанс",
"null_id": "ID сессии равен null"
}
},
"label_one": "Сессия",

View File

@ -49,7 +49,8 @@ const Chat: FC<Props> = (props) => {
const { isTopNavbar } = useNavbarPosition()
const chatMaxWidth = useChatMaxWidth()
const { chat } = useRuntime()
const { activeTopicOrSession, activeAgentId, activeSessionId } = chat
const { activeTopicOrSession, activeAgentId, activeSessionIdMap } = chat
const activeSessionId = activeAgentId ? activeSessionIdMap[activeAgentId] : null
const { apiServer } = useSettings()
const mainRef = React.useRef<HTMLDivElement>(null)
@ -147,8 +148,7 @@ const Chat: FC<Props> = (props) => {
if (activeAgentId === null) {
return () => <div> Active Agent ID is invalid.</div>
}
const sessionId = activeSessionId[activeAgentId]
if (!sessionId) {
if (!activeSessionId) {
return () => <div> Active Session ID is invalid.</div>
}
if (!apiServer.enabled) {
@ -158,18 +158,17 @@ const Chat: FC<Props> = (props) => {
</div>
)
}
return () => <AgentSessionMessages agentId={activeAgentId} sessionId={sessionId} />
return () => <AgentSessionMessages agentId={activeAgentId} sessionId={activeSessionId} />
}, [activeAgentId, activeSessionId, apiServer.enabled, t])
const SessionInputBar = useMemo(() => {
if (activeAgentId === null) {
return () => <div> Active Agent ID is invalid.</div>
}
const sessionId = activeSessionId[activeAgentId]
if (!sessionId) {
if (!activeSessionId) {
return () => <div> Active Session ID is invalid.</div>
}
return () => <AgentSessionInputbar agentId={activeAgentId} sessionId={sessionId} />
return () => <AgentSessionInputbar agentId={activeAgentId} sessionId={activeSessionId} />
}, [activeAgentId, activeSessionId])
// TODO: more info
@ -235,10 +234,8 @@ const Chat: FC<Props> = (props) => {
</>
)}
{activeTopicOrSession === 'session' && !activeAgentId && <AgentInvalid />}
{activeTopicOrSession === 'session' && activeAgentId && !activeSessionId[activeAgentId] && (
<SessionInvalid />
)}
{activeTopicOrSession === 'session' && activeAgentId && activeSessionId[activeAgentId] && (
{activeTopicOrSession === 'session' && activeAgentId && !activeSessionId && <SessionInvalid />}
{activeTopicOrSession === 'session' && activeAgentId && activeSessionId && (
<>
<SessionMessages />
<SessionInputBar />

View File

@ -64,6 +64,14 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
})
}
// const handleUpdateModel = useCallback(
// async (model: ApiModel) => {
// if (!activeSession || !activeAgent) return
// return updateModel(activeSession.id, model.id, { showSuccessToast: false })
// },
// [activeAgent, activeSession, updateModel]
// )
return (
<NavbarHeader className="home-navbar">
<div className="flex min-w-0 flex-1 shrink items-center overflow-auto">

View File

@ -117,7 +117,7 @@ const HomePage: FC = () => {
type: 'chat'
})
} else if (activeTopicOrSession === 'topic') {
dispatch(setActiveAgentId('fake'))
dispatch(setActiveAgentId(null))
}
}, [activeTopicOrSession, dispatch, setActiveAssistant])

View File

@ -1,40 +0,0 @@
import { Button, Divider } from '@heroui/react'
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
import { AgentSettingsPopup } from '@renderer/pages/settings/AgentSettings'
import AdvancedSettings from '@renderer/pages/settings/AgentSettings/AdvancedSettings'
import AgentEssentialSettings from '@renderer/pages/settings/AgentSettings/AgentEssentialSettings'
import { GetAgentResponse } from '@renderer/types/agent'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
agent: GetAgentResponse | undefined | null
update: ReturnType<typeof useUpdateAgent>['updateAgent']
}
const AgentSettingsTab: FC<Props> = ({ agent, update }) => {
const { t } = useTranslation()
const onMoreSetting = () => {
if (agent?.id) {
AgentSettingsPopup.show({ agentId: agent.id! })
}
}
if (!agent) {
return null
}
return (
<div className="w-[var(--assistants-width)] p-2 px-3 pt-4">
<AgentEssentialSettings agent={agent} update={update} showModelSetting={false} />
<AdvancedSettings agentBase={agent} update={update} />
<Divider className="my-2" />
<Button size="sm" fullWidth onPress={onMoreSetting}>
{t('settings.moresetting.label')}
</Button>
</div>
)
}
export default AgentSettingsTab

View File

@ -11,7 +11,7 @@ import { useAppDispatch } from '@renderer/store'
import { addIknowAction } from '@renderer/store/runtime'
import { Assistant, AssistantsSortType } from '@renderer/types'
import { getErrorMessage } from '@renderer/utils'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { FC, useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -80,12 +80,6 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
updateAssistants
})
useEffect(() => {
if (!agentsLoading && agents.length > 0 && !activeAgentId && apiServerConfig.enabled) {
setActiveAgentId(agents[0].id)
}
}, [agentsLoading, agents, activeAgentId, setActiveAgentId, apiServerConfig.enabled])
const onDeleteAssistant = useCallback(
(assistant: Assistant) => {
const remaining = assistants.filter((a) => a.id !== assistant.id)

View File

@ -0,0 +1,43 @@
import { Button, Divider } from '@heroui/react'
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
import { SessionSettingsPopup } from '@renderer/pages/settings/AgentSettings'
import AdvancedSettings from '@renderer/pages/settings/AgentSettings/AdvancedSettings'
import EssentialSettings from '@renderer/pages/settings/AgentSettings/EssentialSettings'
import { GetAgentSessionResponse } from '@renderer/types'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
session: GetAgentSessionResponse | undefined | null
update: ReturnType<typeof useUpdateSession>['updateSession']
}
const SessionSettingsTab: FC<Props> = ({ session, update }) => {
const { t } = useTranslation()
const onMoreSetting = () => {
if (session?.id) {
SessionSettingsPopup.show({
agentId: session.agent_id,
sessionId: session.id
})
}
}
if (!session) {
return null
}
return (
<div className="w-[var(--assistants-width)] p-2 px-3 pt-4">
<EssentialSettings agentBase={session} update={update} />
<AdvancedSettings agentBase={session} update={update} />
<Divider className="my-2" />
<Button size="sm" fullWidth onPress={onMoreSetting}>
{t('settings.moresetting.label')}
</Button>
</div>
)
}
export default SessionSettingsTab

View File

@ -1,71 +0,0 @@
import { Alert, Button, Spinner } from '@heroui/react'
import { AgentModal } from '@renderer/components/Popups/agent/AgentModal'
import { useAgents } from '@renderer/hooks/agents/useAgents'
import { useAgentSessionInitializer } from '@renderer/hooks/agents/useAgentSessionInitializer'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useAppDispatch } from '@renderer/store'
import { setActiveAgentId as setActiveAgentIdAction } from '@renderer/store/runtime'
import { Plus } from 'lucide-react'
import { FC, useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import AgentItem from './AgentItem'
interface AssistantsTabProps {}
export const Agents: FC<AssistantsTabProps> = () => {
const { agents, deleteAgent, isLoading, error } = useAgents()
const { t } = useTranslation()
const { chat } = useRuntime()
const { activeAgentId } = chat
const { initializeAgentSession } = useAgentSessionInitializer()
const dispatch = useAppDispatch()
const setActiveAgentId = useCallback(
async (id: string) => {
dispatch(setActiveAgentIdAction(id))
// Initialize the session for this agent
await initializeAgentSession(id)
},
[dispatch, initializeAgentSession]
)
useEffect(() => {
if (!isLoading && agents.length > 0 && !activeAgentId) {
setActiveAgentId(agents[0].id)
}
}, [isLoading, agents, activeAgentId, setActiveAgentId])
return (
<>
{isLoading && <Spinner />}
{error && <Alert color="danger" title={t('agent.list.error.failed')} />}
{!isLoading &&
!error &&
agents.map((agent) => (
<AgentItem
key={agent.id}
agent={agent}
isActive={agent.id === activeAgentId}
onDelete={() => deleteAgent(agent.id)}
onPress={() => {
setActiveAgentId(agent.id)
}}
/>
))}
<AgentModal
trigger={{
content: (
<Button
onPress={(e) => e.continuePropagation()}
startContent={<Plus size={16} className="mr-1 shrink-0 translate-x-[-2px]" />}
className="w-full justify-start bg-transparent text-foreground-500 hover:bg-[var(--color-list-item)]">
{t('agent.add.title')}
</Button>
)
}}
/>
</>
)
}

View File

@ -1,208 +0,0 @@
import { DownOutlined, RightOutlined } from '@ant-design/icons'
import { Button } from '@heroui/react'
import { DraggableList } from '@renderer/components/DraggableList'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
import { useTags } from '@renderer/hooks/useTags'
import { Assistant, AssistantsSortType } from '@renderer/types'
import { Tooltip } from 'antd'
import { Plus } from 'lucide-react'
import { FC, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import AssistantItem from './AssistantItem'
import { SectionName } from './SectionName'
interface AssistantsProps {
activeAssistant: Assistant
setActiveAssistant: (assistant: Assistant) => void
onCreateAssistant: () => void
onCreateDefaultAssistant: () => void
}
const Assistants: FC<AssistantsProps> = ({
activeAssistant,
setActiveAssistant,
onCreateAssistant,
onCreateDefaultAssistant
}) => {
const { assistants, removeAssistant, copyAssistant, updateAssistants } = useAssistants()
const [dragging, setDragging] = useState(false)
const { addAssistantPreset } = useAssistantPresets()
const { t } = useTranslation()
const { getGroupedAssistants, collapsedTags, toggleTagCollapse } = useTags()
const { assistantsTabSortType = 'list', setAssistantsTabSortType } = useAssistantsTabSortType()
const onDelete = useCallback(
(assistant: Assistant) => {
const remaining = assistants.filter((a) => a.id !== assistant.id)
if (assistant.id === activeAssistant?.id) {
const newActive = remaining[remaining.length - 1]
newActive ? setActiveAssistant(newActive) : onCreateDefaultAssistant()
}
removeAssistant(assistant.id)
},
[activeAssistant, assistants, removeAssistant, setActiveAssistant, onCreateDefaultAssistant]
)
const handleSortByChange = useCallback(
(sortType: AssistantsSortType) => {
setAssistantsTabSortType(sortType)
},
[setAssistantsTabSortType]
)
const handleGroupReorder = useCallback(
(tag: string, newGroupList: Assistant[]) => {
let insertIndex = 0
const newGlobal = assistants.map((a) => {
const tags = a.tags?.length ? a.tags : [t('assistants.tags.untagged')]
if (tags.includes(tag)) {
const replaced = newGroupList[insertIndex]
insertIndex += 1
return replaced
}
return a
})
updateAssistants(newGlobal)
},
[assistants, t, updateAssistants]
)
const renderAddAssistantButton = useMemo(() => {
return (
<Button
onPress={onCreateAssistant}
className="w-full justify-start bg-transparent text-foreground-500 hover:bg-[var(--color-list-item)]">
<Plus size={16} style={{ marginRight: 4, flexShrink: 0 }} />
{t('chat.add.assistant.title')}
</Button>
)
}, [onCreateAssistant, t])
if (assistantsTabSortType === 'tags') {
return (
<>
<SectionName name={t('common.assistant_other')} />
<div style={{ marginBottom: '8px' }}>
{getGroupedAssistants.map((group) => (
<TagsContainer key={group.tag}>
{group.tag !== t('assistants.tags.untagged') && (
<GroupTitle onClick={() => toggleTagCollapse(group.tag)}>
<Tooltip title={group.tag}>
<GroupTitleName>
{collapsedTags[group.tag] ? (
<RightOutlined style={{ fontSize: '10px', marginRight: '5px' }} />
) : (
<DownOutlined style={{ fontSize: '10px', marginRight: '5px' }} />
)}
{group.tag}
</GroupTitleName>
</Tooltip>
<GroupTitleDivider />
</GroupTitle>
)}
{!collapsedTags[group.tag] && (
<div>
<DraggableList
list={group.assistants}
onUpdate={(newList) => handleGroupReorder(group.tag, newList)}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}>
{(assistant) => (
<AssistantItem
key={assistant.id}
assistant={assistant}
isActive={assistant.id === activeAssistant.id}
sortBy={assistantsTabSortType}
onSwitch={setActiveAssistant}
onDelete={onDelete}
addPreset={addAssistantPreset}
copyAssistant={copyAssistant}
onCreateDefaultAssistant={onCreateDefaultAssistant}
handleSortByChange={handleSortByChange}
/>
)}
</DraggableList>
</div>
)}
</TagsContainer>
))}
{renderAddAssistantButton}
</div>
</>
)
}
return (
<div>
<SectionName name={t('common.assistant_other')} />
<DraggableList
list={assistants}
onUpdate={updateAssistants}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}>
{(assistant) => (
<AssistantItem
key={assistant.id}
assistant={assistant}
isActive={assistant.id === activeAssistant.id}
sortBy={assistantsTabSortType}
onSwitch={setActiveAssistant}
onDelete={onDelete}
addPreset={addAssistantPreset}
copyAssistant={copyAssistant}
onCreateDefaultAssistant={onCreateDefaultAssistant}
handleSortByChange={handleSortByChange}
/>
)}
</DraggableList>
{!dragging && renderAddAssistantButton}
<div style={{ minHeight: 10 }}></div>
</div>
)
}
// 样式组件
const TagsContainer = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
`
const GroupTitle = styled.div`
color: var(--color-text-2);
font-size: 12px;
font-weight: 500;
cursor: pointer;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 24px;
margin: 5px 0;
`
const GroupTitleName = styled.div`
max-width: 50%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
box-sizing: border-box;
padding: 0 4px;
color: var(--color-text);
font-size: 13px;
line-height: 24px;
margin-right: 5px;
display: flex;
`
const GroupTitleDivider = styled.div`
flex: 1;
border-top: 1px solid var(--color-border);
`
export default Assistants

View File

@ -33,8 +33,8 @@ interface SessionItemProps {
const SessionItem: FC<SessionItemProps> = ({ session, agentId, isDisabled, isLoading, onDelete, onPress }) => {
const { t } = useTranslation()
const { chat } = useRuntime()
const updateSession = useUpdateSession(agentId)
const activeSessionId = chat.activeSessionId[agentId]
const { updateSession } = useUpdateSession(agentId)
const activeSessionId = chat.activeSessionIdMap[agentId]
const [isConfirmingDeletion, setIsConfirmingDeletion] = useState(false)
const { setTimeoutTimer } = useTimer()
const dispatch = useAppDispatch()

View File

@ -30,7 +30,7 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
const { agent } = useAgent(agentId)
const { sessions, isLoading, error, deleteSession, createSession } = useSessions(agentId)
const { chat } = useRuntime()
const { activeSessionId, sessionWaiting } = chat
const { activeSessionIdMap, sessionWaiting } = chat
const dispatch = useAppDispatch()
const setActiveSessionId = useCallback(
@ -75,24 +75,24 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
[agentId, deleteSession, dispatch, sessions, t]
)
const currentActiveSessionId = activeSessionId[agentId]
const activeSessionId = activeSessionIdMap[agentId]
useEffect(() => {
if (!isLoading && sessions.length > 0 && !currentActiveSessionId) {
if (!isLoading && sessions.length > 0 && !activeSessionId) {
setActiveSessionId(agentId, sessions[0].id)
}
}, [isLoading, sessions, currentActiveSessionId, agentId, setActiveSessionId])
}, [isLoading, sessions, activeSessionId, agentId, setActiveSessionId])
useEffect(() => {
if (currentActiveSessionId) {
if (activeSessionId) {
dispatch(
newMessagesActions.setTopicFulfilled({
topicId: buildAgentSessionTopicId(currentActiveSessionId),
topicId: buildAgentSessionTopicId(activeSessionId),
fulfilled: false
})
)
}
}, [currentActiveSessionId, dispatch])
}, [activeSessionId, dispatch])
if (isLoading) {
return (

View File

@ -1,6 +1,7 @@
import { Alert, Skeleton } from '@heroui/react'
import AddAssistantPopup from '@renderer/components/Popups/AddAssistantPopup'
import { useAgent } from '@renderer/hooks/agents/useAgent'
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
import { useActiveSession } from '@renderer/hooks/agents/useActiveSession'
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
@ -8,13 +9,13 @@ import { useShowTopics } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Assistant, Topic } from '@renderer/types'
import { Tab } from '@renderer/types/chat'
import { classNames, uuid } from '@renderer/utils'
import { classNames, getErrorMessage, uuid } from '@renderer/utils'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import AgentSettingsTab from './AgentSettingsTab'
import Assistants from './AssistantsTab'
import SessionSettingsTab from './SessionSettingsTab'
import Settings from './SettingsTab'
import Topics from './TopicsTab'
@ -47,8 +48,8 @@ const HomeTabs: FC<Props> = ({
const { t } = useTranslation()
const { chat } = useRuntime()
const { activeTopicOrSession, activeAgentId } = chat
const { agent } = useAgent(activeAgentId)
const { updateAgent } = useUpdateAgent()
const { session, isLoading: isSessionLoading, error: sessionError } = useActiveSession()
const { updateSession } = useUpdateSession(activeAgentId)
const isSessionView = activeTopicOrSession === 'session'
const isTopicView = activeTopicOrSession === 'topic'
@ -125,7 +126,7 @@ const HomeTabs: FC<Props> = ({
</CustomTabs>
)}
{position === 'right' && topicPosition === 'right' && isTopicView && (
{position === 'right' && topicPosition === 'right' && (
<CustomTabs>
<TabItem active={tab === 'topic'} onClick={() => setTab('topic')}>
{t('common.topics')}
@ -154,7 +155,20 @@ const HomeTabs: FC<Props> = ({
/>
)}
{tab === 'settings' && isTopicView && <Settings assistant={activeAssistant} />}
{tab === 'settings' && isSessionView && <AgentSettingsTab agent={agent} update={updateAgent} />}
{tab === 'settings' && isSessionView && !sessionError && (
<Skeleton isLoaded={!isSessionLoading} className="h-full">
<SessionSettingsTab session={session} update={updateSession} />
</Skeleton>
)}
{tab === 'settings' && isSessionView && sessionError && (
<div className="w-[var(--assistants-width)] p-2 px-3 pt-4">
<Alert
color="danger"
title={t('agent.session.get.error.failed')}
description={getErrorMessage(sessionError)}
/>
</div>
)}
</TabContent>
</Container>
)

View File

@ -1,18 +1,18 @@
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 { useActiveAgent } from '@renderer/hooks/agents/useActiveAgent'
import { useActiveSession } from '@renderer/hooks/agents/useActiveSession'
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { ApiModel, Assistant, PermissionMode } from '@renderer/types'
import { AgentEntity, AgentSessionEntity, 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 { AgentSettingsPopup, SessionSettingsPopup } from '../../settings/AgentSettings'
import { AgentLabel, SessionLabel } from '../../settings/AgentSettings/shared'
import SelectAgentBaseModelButton from './SelectAgentBaseModelButton'
import SelectModelButton from './SelectModelButton'
interface Props {
@ -21,41 +21,67 @@ interface Props {
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 { activeTopicOrSession } = chat
const { agent: activeAgent } = useActiveAgent()
const { session: activeSession } = useActiveSession()
const { updateModel } = useUpdateSession(activeAgent?.id ?? null)
const handleUpdateModel = useCallback(
async (model: ApiModel) => {
if (!agent) return
return updateModel(agent.id, model.id, { showSuccessToast: false })
if (!activeAgent || !activeSession) return
return updateModel(activeSession.id, model.id, { showSuccessToast: false })
},
[agent, updateModel]
[activeAgent, activeSession, updateModel]
)
return (
<>
{activeTopicOrSession === 'topic' && <SelectModelButton assistant={assistant} />}
{activeTopicOrSession === 'session' && agent && (
{activeTopicOrSession === 'session' && activeAgent && (
<HorizontalScrollContainer>
<Breadcrumbs classNames={{ base: 'flex', list: 'flex-nowrap' }}>
<Breadcrumbs
classNames={{
base: 'flex',
list: 'flex-nowrap'
}}>
<BreadcrumbItem
onPress={() => AgentSettingsPopup.show({ agentId: agent.id })}
classNames={{ base: 'self-stretch', item: 'h-full' }}>
onPress={() => AgentSettingsPopup.show({ agentId: activeAgent.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' }}
agent={activeAgent}
classNames={{ name: 'max-w-40 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 && (
{activeSession && (
<BreadcrumbItem
onPress={() =>
SessionSettingsPopup.show({
agentId: activeAgent.id,
sessionId: activeSession.id
})
}
classNames={{
base: 'self-stretch',
item: 'h-full'
}}>
<Chip size="md" variant="light" className="h-full transition-background hover:bg-foreground-100">
<SessionLabel session={activeSession} className="max-w-40 font-bold text-xs" />
</Chip>
</BreadcrumbItem>
)}
{activeSession && (
<BreadcrumbItem>
<SessionWorkspaceMeta agentId={activeAgentId} sessionId={sessionId} />
<SelectAgentBaseModelButton agentBase={activeSession} onSelect={handleUpdateModel} />
</BreadcrumbItem>
)}
{activeAgent && activeSession && (
<BreadcrumbItem>
<SessionWorkspaceMeta agent={activeAgent} session={activeSession} />
</BreadcrumbItem>
)}
</Breadcrumbs>
@ -65,9 +91,7 @@ const ChatNavbarContent: FC<Props> = ({ assistant }) => {
)
}
const SessionWorkspaceMeta: FC<{ agentId: string; sessionId: string }> = ({ agentId, sessionId }) => {
const { agent } = useAgent(agentId)
const { session } = useSession(agentId, sessionId)
const SessionWorkspaceMeta: FC<{ agent: AgentEntity; session: AgentSessionEntity }> = ({ agent, session }) => {
if (!session || !agent) {
return null
}

View File

@ -12,12 +12,12 @@ import { FC } from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
agent: AgentBaseWithId
agentBase: AgentBaseWithId
onSelect: (model: ApiModel) => Promise<void>
isDisabled?: boolean
}
const SelectAgentModelButton: FC<Props> = ({ agent, onSelect, isDisabled }) => {
const SelectAgentBaseModelButton: FC<Props> = ({ agentBase: agent, onSelect, isDisabled }) => {
const { t } = useTranslation()
const model = useApiModel({ id: agent?.model })
@ -42,9 +42,9 @@ const SelectAgentModelButton: FC<Props> = ({ agent, onSelect, isDisabled }) => {
className="nodrag rounded-2xl px-1 py-3"
onPress={onSelectModel}
isDisabled={isDisabled}>
<div className="flex items-center gap-1.5">
<div className="flex items-center gap-1.5 overflow-x-hidden">
<ModelAvatar model={model ? apiModelAdapter(model) : undefined} size={20} />
<span className="-mr-0.5 font-medium">
<span className="truncate font-medium">
{model ? model.name : t('button.select_model')} {providerName ? ' | ' + providerName : ''}
</span>
</div>
@ -53,4 +53,4 @@ const SelectAgentModelButton: FC<Props> = ({ agent, onSelect, isDisabled }) => {
)
}
export default SelectAgentModelButton
export default SelectAgentBaseModelButton

View File

@ -23,7 +23,7 @@ type AdvancedSettingsProps =
}
| {
agentBase: GetAgentSessionResponse | undefined | null
update: ReturnType<typeof useUpdateSession>
update: ReturnType<typeof useUpdateSession>['updateSession']
}
const defaultConfiguration: AgentConfigurationState = AgentConfigurationSchema.parse({})

View File

@ -1,47 +0,0 @@
import { Avatar } from '@heroui/react'
import { getAgentTypeAvatar } from '@renderer/config/agent'
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
import { getAgentTypeLabel } from '@renderer/i18n/label'
import { GetAgentResponse } from '@renderer/types'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { AccessibleDirsSetting } from './AccessibleDirsSetting'
import { AvatarSetting } from './AvatarSetting'
import { DescriptionSetting } from './DescriptionSetting'
import { ModelSetting } from './ModelSetting'
import { NameSetting } from './NameSetting'
import { SettingsContainer, SettingsItem, SettingsTitle } from './shared'
// const logger = loggerService.withContext('AgentEssentialSettings')
interface AgentEssentialSettingsProps {
agent: GetAgentResponse | undefined | null
update: ReturnType<typeof useUpdateAgent>['updateAgent']
showModelSetting?: boolean
}
const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update, showModelSetting = true }) => {
const { t } = useTranslation()
if (!agent) return null
return (
<SettingsContainer>
<SettingsItem inline>
<SettingsTitle>{t('agent.type.label')}</SettingsTitle>
<div className="flex items-center gap-2">
<Avatar src={getAgentTypeAvatar(agent.type)} className="h-6 w-6 text-lg" />
<span>{(agent?.name ?? agent?.type) ? getAgentTypeLabel(agent.type) : ''}</span>
</div>
</SettingsItem>
<AvatarSetting agent={agent} update={update} />
<NameSetting base={agent} update={update} />
{showModelSetting && <ModelSetting base={agent} update={update} />}
<AccessibleDirsSetting base={agent} update={update} />
<DescriptionSetting base={agent} update={update} />
</SettingsContainer>
)
}
export default AgentEssentialSettings

View File

@ -6,7 +6,7 @@ import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import AdvancedSettings from './AdvancedSettings'
import AgentEssentialSettings from './AgentEssentialSettings'
import EssentialSettings from './EssentialSettings'
import PromptSettings from './PromptSettings'
import { AgentLabel, LeftMenu, Settings, StyledMenu, StyledModal } from './shared'
import ToolingSettings from './ToolingSettings'
@ -87,7 +87,7 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
/>
</LeftMenu>
<Settings>
{menu === 'essential' && <AgentEssentialSettings agent={agent} update={updateAgent} />}
{menu === 'essential' && <EssentialSettings agentBase={agent} update={updateAgent} />}
{menu === 'prompt' && <PromptSettings agentBase={agent} update={updateAgent} />}
{menu === 'tooling' && <ToolingSettings agentBase={agent} update={updateAgent} />}
{menu === 'advanced' && <AdvancedSettings agentBase={agent} update={updateAgent} />}

View File

@ -0,0 +1,56 @@
import { Avatar } from '@heroui/react'
import { getAgentTypeAvatar } from '@renderer/config/agent'
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
import { getAgentTypeLabel } from '@renderer/i18n/label'
import { GetAgentResponse, GetAgentSessionResponse, isAgentEntity } from '@renderer/types'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { AccessibleDirsSetting } from './AccessibleDirsSetting'
import { AvatarSetting } from './AvatarSetting'
import { DescriptionSetting } from './DescriptionSetting'
import { ModelSetting } from './ModelSetting'
import { NameSetting } from './NameSetting'
import { SettingsContainer, SettingsItem, SettingsTitle } from './shared'
// const logger = loggerService.withContext('AgentEssentialSettings')
type EssentialSettingsProps =
| {
agentBase: GetAgentResponse | undefined | null
update: ReturnType<typeof useUpdateAgent>['updateAgent']
}
| {
agentBase: GetAgentSessionResponse | undefined | null
update: ReturnType<typeof useUpdateSession>['updateSession']
}
const EssentialSettings: FC<EssentialSettingsProps> = ({ agentBase, update }) => {
const { t } = useTranslation()
if (!agentBase) return null
const isAgent = isAgentEntity(agentBase)
return (
<SettingsContainer>
{isAgent && (
<SettingsItem inline>
<SettingsTitle>{t('agent.type.label')}</SettingsTitle>
<div className="flex items-center gap-2">
<Avatar src={getAgentTypeAvatar(agentBase.type)} className="h-6 w-6 text-lg" />
<span>{(agentBase?.name ?? agentBase?.type) ? getAgentTypeLabel(agentBase.type) : ''}</span>
</div>
</SettingsItem>
)}
{isAgent && <AvatarSetting agent={agentBase} update={update} />}
<NameSetting base={agentBase} update={update} />
<ModelSetting base={agentBase} update={update} />
<AccessibleDirsSetting base={agentBase} update={update} />
<DescriptionSetting base={agentBase} update={update} />
</SettingsContainer>
)
}
export default EssentialSettings

View File

@ -1,4 +1,4 @@
import SelectAgentModelButton from '@renderer/pages/home/components/SelectAgentModelButton'
import SelectAgentBaseModelButton from '@renderer/pages/home/components/SelectAgentBaseModelButton'
import { AgentBaseWithId, ApiModel, UpdateAgentBaseForm } from '@renderer/types'
import { useTranslation } from 'react-i18next'
@ -21,9 +21,9 @@ export const ModelSetting: React.FC<ModelSettingProps> = ({ base, update, isDisa
if (!base) return null
return (
<SettingsItem inline className="gap-8">
<SettingsItem inline>
<SettingsTitle id="model">{t('common.model')}</SettingsTitle>
<SelectAgentModelButton agent={base} onSelect={updateModel} isDisabled={isDisabled} />
<SelectAgentBaseModelButton agentBase={base} onSelect={updateModel} isDisabled={isDisabled} />
</SettingsItem>
)
}

View File

@ -22,7 +22,7 @@ type AgentPromptSettingsProps =
}
| {
agentBase: AgentSessionEntity | undefined | null
update: ReturnType<typeof useUpdateSession>
update: ReturnType<typeof useUpdateSession>['updateSession']
}
const PromptSettings: FC<AgentPromptSettingsProps> = ({ agentBase, update }) => {

View File

@ -1,29 +0,0 @@
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
import { GetAgentSessionResponse } from '@renderer/types'
import { FC } from 'react'
import { AccessibleDirsSetting } from './AccessibleDirsSetting'
import { DescriptionSetting } from './DescriptionSetting'
import { NameSetting } from './NameSetting'
import { SettingsContainer } from './shared'
// const logger = loggerService.withContext('AgentEssentialSettings')
interface SessionEssentialSettingsProps {
session: GetAgentSessionResponse | undefined | null
update: ReturnType<typeof useUpdateAgent>['updateAgent']
}
const SessionEssentialSettings: FC<SessionEssentialSettingsProps> = ({ session, update }) => {
if (!session) return null
return (
<SettingsContainer>
<NameSetting base={session} update={update} />
<AccessibleDirsSetting base={session} update={update} />
<DescriptionSetting base={session} update={update} />
</SettingsContainer>
)
}
export default SessionEssentialSettings

View File

@ -6,8 +6,8 @@ import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import AdvancedSettings from './AdvancedSettings'
import EssentialSettings from './EssentialSettings'
import PromptSettings from './PromptSettings'
import SessionEssentialSettings from './SessionEssentialSettings'
import { LeftMenu, SessionLabel, Settings, StyledMenu, StyledModal } from './shared'
import ToolingSettings from './ToolingSettings'
@ -30,7 +30,7 @@ const SessionSettingPopupContainer: React.FC<SessionSettingPopupParams> = ({ tab
const { session, isLoading, error } = useSession(agentId, sessionId)
const updateSession = useUpdateSession(agentId)
const { updateSession } = useUpdateSession(agentId)
const onOk = () => {
setOpen(false)
@ -89,7 +89,7 @@ const SessionSettingPopupContainer: React.FC<SessionSettingPopupParams> = ({ tab
/>
</LeftMenu>
<Settings>
{menu === 'essential' && <SessionEssentialSettings session={session} update={updateSession} />}
{menu === 'essential' && <EssentialSettings agentBase={session} update={updateSession} />}
{menu === 'prompt' && <PromptSettings agentBase={session} update={updateSession} />}
{menu === 'tooling' && <ToolingSettings agentBase={session} update={updateSession} />}
{menu === 'advanced' && <AdvancedSettings agentBase={session} update={updateSession} />}

View File

@ -1,5 +1,4 @@
import { cn } from '@heroui/react'
import Ellipsis from '@renderer/components/Ellipsis'
import EmojiIcon from '@renderer/components/EmojiIcon'
import { getAgentTypeLabel } from '@renderer/i18n/label'
import { AgentEntity, AgentSessionEntity } from '@renderer/types'
@ -35,11 +34,11 @@ export const AgentLabel: React.FC<AgentLabelProps> = ({ agent, classNames }) =>
const emoji = agent?.configuration?.avatar
return (
<div className={cn('flex w-full items-center gap-2', classNames?.container)}>
<div className={cn('flex w-full items-center gap-2 truncate', classNames?.container)}>
<EmojiIcon emoji={emoji || '⭐️'} className={classNames?.avatar} />
<Ellipsis className={classNames?.name}>
<span className={cn('truncate', classNames?.name)}>
{agent?.name ?? (agent?.type ? getAgentTypeLabel(agent.type) : '')}
</Ellipsis>
</span>
</div>
)
}
@ -53,7 +52,7 @@ export const SessionLabel: React.FC<SessionLabelProps> = ({ session, className }
const displayName = session?.name ?? session?.id
return (
<>
<span className={cn('px-2 text-sm', className)}>{displayName}</span>
<span className={cn('truncate px-2 text-sm', className)}>{displayName}</span>
</>
)
}

View File

@ -11,7 +11,7 @@ export interface ChatState {
activeAgentId: string | null
/** UI state. Map agent id to active session id.
* null represents no active session */
activeSessionId: Record<string, string | null>
activeSessionIdMap: Record<string, string | null>
/** meanwhile active Assistants or Agents */
activeTopicOrSession: 'topic' | 'session'
/** topic ids that are currently being renamed */
@ -90,7 +90,7 @@ const initialState: RuntimeState = {
activeTopic: null,
activeAgentId: null,
activeTopicOrSession: 'topic',
activeSessionId: {},
activeSessionIdMap: {},
renamingTopics: [],
newlyRenamedTopics: [],
sessionWaiting: {}
@ -163,7 +163,7 @@ const runtimeSlice = createSlice({
},
setActiveSessionIdAction: (state, action: PayloadAction<{ agentId: string; sessionId: string | null }>) => {
const { agentId, sessionId } = action.payload
state.chat.activeSessionId[agentId] = sessionId
state.chat.activeSessionIdMap[agentId] = sessionId
},
setActiveTopicOrSessionAction: (state, action: PayloadAction<'topic' | 'session'>) => {
state.chat.activeTopicOrSession = action.payload

View File

@ -266,8 +266,12 @@ export const GetAgentSessionResponseSchema = AgentSessionEntitySchema.extend({
slash_commands: z.array(SlashCommandSchema).optional() // Array of slash commands to trigger the agent
})
export const CreateAgentSessionResponseSchema = GetAgentSessionResponseSchema
export type GetAgentSessionResponse = z.infer<typeof GetAgentSessionResponseSchema>
export type CreateAgentSessionResponse = GetAgentSessionResponse
export const ListAgentSessionsResponseSchema = z.object({
data: z.array(AgentSessionEntitySchema),
total: z.int(),