diff --git a/src/main/apiServer/app.ts b/src/main/apiServer/app.ts index 1046399954..0c6615f1e8 100644 --- a/src/main/apiServer/app.ts +++ b/src/main/apiServer/app.ts @@ -15,6 +15,11 @@ import { modelsRoutes } from './routes/models' const logger = loggerService.withContext('ApiServer') const app = express() +app.use( + express.json({ + limit: '50mb' + }) +) // Global middleware app.use((req, res, next) => { @@ -111,7 +116,6 @@ app.get('/', (_req, res) => { // Provider-specific API routes with auth (must be before /v1 to avoid conflicts) const providerRouter = express.Router({ mergeParams: true }) providerRouter.use(authMiddleware) -providerRouter.use(express.json()) // Mount provider-specific messages route providerRouter.use('/v1/messages', messagesProviderRoutes) app.use('/:provider', providerRouter) @@ -119,7 +123,6 @@ app.use('/:provider', providerRouter) // API v1 routes with auth const apiRouter = express.Router() apiRouter.use(authMiddleware) -apiRouter.use(express.json()) // Mount routes apiRouter.use('/chat', chatRoutes) apiRouter.use('/mcps', mcpRoutes) diff --git a/src/main/apiServer/utils/index.ts b/src/main/apiServer/utils/index.ts index eebf226090..8f3f36a30b 100644 --- a/src/main/apiServer/utils/index.ts +++ b/src/main/apiServer/utils/index.ts @@ -7,7 +7,7 @@ const logger = loggerService.withContext('ApiServerUtils') // Cache configuration const PROVIDERS_CACHE_KEY = 'api-server:providers' -const PROVIDERS_CACHE_TTL = 5 * 60 * 1000 // 5 minutes +const PROVIDERS_CACHE_TTL = 1 * 60 * 1000 // 1 minutes export async function getAvailableProviders(): Promise { try { diff --git a/src/main/services/agents/services/claudecode/transform.ts b/src/main/services/agents/services/claudecode/transform.ts index 8c0d429a14..32a52875f6 100644 --- a/src/main/services/agents/services/claudecode/transform.ts +++ b/src/main/services/agents/services/claudecode/transform.ts @@ -34,7 +34,7 @@ const generateMessageId = (): string => `msg_${uuidv4().replace(/-/g, '')}` // Main transform function export function transformSDKMessageToStreamParts(sdkMessage: SDKMessage): AgentStreamPart[] { const chunks: AgentStreamPart[] = [] - logger.debug('Transforming SDKMessage to stream parts', sdkMessage) + logger.silly('Transforming SDKMessage to stream parts', sdkMessage) switch (sdkMessage.type) { case 'assistant': case 'user': @@ -105,7 +105,6 @@ function handleUserOrAssistantMessage(message: Extract): AgentStreamPart[] { const chunks: AgentStreamPart[] = [] - logger.debug('Received system message', { - subtype: message.subtype - }) switch (message.subtype) { case 'init': { chunks.push({ diff --git a/src/renderer/src/components/Popups/agent/SessionModal.tsx b/src/renderer/src/components/Popups/agent/SessionModal.tsx index cf5750c124..64f8b1e628 100644 --- a/src/renderer/src/components/Popups/agent/SessionModal.tsx +++ b/src/renderer/src/components/Popups/agent/SessionModal.tsx @@ -20,6 +20,7 @@ import { getModelLogo } from '@renderer/config/models' import { useAgent } from '@renderer/hooks/agents/useAgent' import { useApiModels } from '@renderer/hooks/agents/useModels' import { useSessions } from '@renderer/hooks/agents/useSessions' +import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession' import { AgentEntity, AgentSessionEntity, BaseSessionForm, CreateSessionForm, UpdateSessionForm } from '@renderer/types' import { ChangeEvent, FormEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -86,7 +87,8 @@ export const SessionModal: React.FC = ({ const { t } = useTranslation() const loadingRef = useRef(false) // const { setTimeoutTimer } = useTimer() - const { createSession, updateSession } = useSessions(agentId) + const { createSession } = useSessions(agentId) + const updateSession = useUpdateSession(agentId) // Only support claude code for now const { models } = useApiModels({ providerType: 'anthropic' }) const { agent } = useAgent(agentId) @@ -164,7 +166,7 @@ export const SessionModal: React.FC = ({ } if (form.accessible_paths.length === 0) { - window.toast.error(t('agent.session.accessible_paths.required')) + window.toast.error(t('agent.session.accessible_paths.error.at_least_one')) loadingRef.current = false return } diff --git a/src/renderer/src/hooks/agents/useSessions.ts b/src/renderer/src/hooks/agents/useSessions.ts index 9e83ac5afc..3307d0353d 100644 --- a/src/renderer/src/hooks/agents/useSessions.ts +++ b/src/renderer/src/hooks/agents/useSessions.ts @@ -1,4 +1,4 @@ -import { CreateSessionForm, UpdateSessionForm } from '@renderer/types' +import { CreateSessionForm } from '@renderer/types' import { formatErrorMessageWithPrefix } from '@renderer/utils/error' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' @@ -46,25 +46,14 @@ export const useSessions = (agentId: string) => { const deleteSession = useCallback( async (id: string) => { - if (!agentId) return + if (!agentId) return false try { await client.deleteSession(agentId, id) mutate((prev) => prev?.filter((session) => session.id !== id)) + return true } catch (error) { window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.delete.error.failed'))) - } - }, - [agentId, client, mutate, t] - ) - - const updateSession = useCallback( - async (form: UpdateSessionForm) => { - if (!agentId) return - try { - const result = await client.updateSession(agentId, form) - mutate((prev) => prev?.map((session) => (session.id === form.id ? result : session))) - } catch (error) { - window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.update.error.failed'))) + return false } }, [agentId, client, mutate, t] @@ -76,7 +65,6 @@ export const useSessions = (agentId: string) => { isLoading, createSession, getSession, - deleteSession, - updateSession + deleteSession } } diff --git a/src/renderer/src/hooks/agents/useUpdateSession.ts b/src/renderer/src/hooks/agents/useUpdateSession.ts new file mode 100644 index 0000000000..4e91c2fda9 --- /dev/null +++ b/src/renderer/src/hooks/agents/useUpdateSession.ts @@ -0,0 +1,36 @@ +import { ListAgentSessionsResponse, UpdateSessionForm } from '@renderer/types' +import { formatErrorMessageWithPrefix } from '@renderer/utils/error' +import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { mutate } from 'swr' + +import { useAgentClient } from './useAgentClient' + +export const useUpdateSession = (agentId: string) => { + const { t } = useTranslation() + const client = useAgentClient() + const paths = client.getSessionPaths(agentId) + const listKey = paths.base + + const updateSession = useCallback( + async (form: UpdateSessionForm) => { + const sessionId = form.id + try { + const itemKey = paths.withId(sessionId) + // may change to optimistic update + const result = await client.updateSession(agentId, form) + mutate( + listKey, + (prev) => prev?.map((session) => (session.id === result.id ? result : session)) ?? [] + ) + mutate(itemKey, result) + window.toast.success(t('common.update_success')) + } catch (error) { + window.toast.error(formatErrorMessageWithPrefix(error, t('agent.update.error.failed'))) + } + }, + [agentId, client, listKey, paths, t] + ) + + return updateSession +} diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 8af40a56e4..b7cba46a9d 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -52,7 +52,8 @@ "delete": { "content": "Are you sure to delete this session?", "error": { - "failed": "Failed to delete the session" + "failed": "Failed to delete the session", + "last": "At least one session must be kept" }, "title": "Delete session" }, @@ -84,6 +85,9 @@ "error": { "failed": "Failed to update the agent" } + }, + "warning": { + "enable_server": "Enable API Server to use agents." } }, "agents": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 592a31612a..d7287036a6 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -52,7 +52,8 @@ "delete": { "content": "确定要删除此会话吗?", "error": { - "failed": "删除会话失败" + "failed": "删除会话失败", + "last": "至少需要保留一个会话" }, "title": "删除会话" }, @@ -84,6 +85,9 @@ "error": { "failed": "更新智能体失败" } + }, + "warning": { + "enable_server": "请启用 API 服务器以使用智能体功能" } }, "agents": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 3e2670ac45..fbe922dbe3 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -52,7 +52,8 @@ "delete": { "content": "您確定要刪除此工作階段嗎?", "error": { - "failed": "無法刪除工作階段" + "failed": "無法刪除工作階段", + "last": "至少必須保留一個工作階段" }, "title": "刪除工作階段" }, @@ -84,6 +85,9 @@ "error": { "failed": "無法更新代理程式" } + }, + "warning": { + "enable_server": "啟用 API 伺服器以使用代理程式。" } }, "agents": { diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index c171834bd8..8af53b669c 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -52,7 +52,8 @@ "delete": { "content": "Είσαι σίγουρος ότι θέλεις να διαγράψεις αυτήν τη συνεδρία;", "error": { - "failed": "Αποτυχία διαγραφής της συνεδρίας" + "failed": "Αποτυχία διαγραφής της συνεδρίας", + "last": "Τουλάχιστον μία συνεδρία πρέπει να διατηρηθεί" }, "title": "Διαγραφή συνεδρίας" }, @@ -84,6 +85,9 @@ "error": { "failed": "Αποτυχία ενημέρωσης του πράκτορα" } + }, + "warning": { + "enable_server": "Ενεργοποίηση του διακομιστή API για χρήση πρακτόρων." } }, "agents": { diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index a0b3e0b911..0be385d740 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -52,7 +52,8 @@ "delete": { "content": "¿Estás seguro de eliminar esta sesión?", "error": { - "failed": "Error al eliminar la sesión" + "failed": "Error al eliminar la sesión", + "last": "Debe mantenerse al menos una sesión" }, "title": "Eliminar sesión" }, @@ -84,6 +85,9 @@ "error": { "failed": "Error al actualizar el agente" } + }, + "warning": { + "enable_server": "Habilitar el servidor API para usar agentes." } }, "agents": { diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 699afd5aaf..93209acb5a 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -52,7 +52,8 @@ "delete": { "content": "Êtes-vous sûr de vouloir supprimer cette session ?", "error": { - "failed": "Échec de la suppression de la session" + "failed": "Échec de la suppression de la session", + "last": "Au moins une session doit être conservée" }, "title": "Supprimer la session" }, @@ -84,6 +85,9 @@ "error": { "failed": "Échec de la mise à jour de l'agent" } + }, + "warning": { + "enable_server": "Permettre au serveur API d'utiliser des agents." } }, "agents": { diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index fc36f737c3..d23b5d15c9 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -52,7 +52,8 @@ "delete": { "content": "このセッションを削除してもよろしいですか?", "error": { - "failed": "セッションの削除に失敗しました" + "failed": "セッションの削除に失敗しました", + "last": "少なくとも1つのセッションを維持する必要があります" }, "title": "セッションを削除" }, @@ -84,6 +85,9 @@ "error": { "failed": "エージェントの更新に失敗しました" } + }, + "warning": { + "enable_server": "APIサーバーがエージェントを使用できるようにする。" } }, "agents": { diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index e87d37fa32..728347565c 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -52,7 +52,8 @@ "delete": { "content": "Tem certeza de que deseja excluir esta sessão?", "error": { - "failed": "Falha ao excluir a sessão" + "failed": "Falha ao excluir a sessão", + "last": "Pelo menos uma sessão deve ser mantida" }, "title": "Excluir sessão" }, @@ -84,6 +85,9 @@ "error": { "failed": "Falha ao atualizar o agente" } + }, + "warning": { + "enable_server": "Ativar o Servidor de API para usar agentes." } }, "agents": { diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 7c98010d35..59ff59243a 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -52,7 +52,8 @@ "delete": { "content": "Вы уверены, что хотите удалить этот сеанс?", "error": { - "failed": "Не удалось удалить сеанс" + "failed": "Не удалось удалить сеанс", + "last": "Должен быть сохранён хотя бы один сеанс" }, "title": "Удалить сеанс" }, @@ -84,6 +85,9 @@ "error": { "failed": "Не удалось обновить агента" } + }, + "warning": { + "enable_server": "Разрешить серверу API использовать агентов." } }, "agents": { diff --git a/src/renderer/src/pages/home/Chat.tsx b/src/renderer/src/pages/home/Chat.tsx index 5290d6285c..d5870add7f 100644 --- a/src/renderer/src/pages/home/Chat.tsx +++ b/src/renderer/src/pages/home/Chat.tsx @@ -1,3 +1,4 @@ +import { Alert } from '@heroui/react' import { loggerService } from '@logger' import { ContentSearch, ContentSearchRef } from '@renderer/components/ContentSearch' import { HStack } from '@renderer/components/Layout' @@ -49,6 +50,7 @@ const Chat: FC = (props) => { const chatMaxWidth = useChatMaxWidth() const { chat } = useRuntime() const { activeTopicOrSession, activeAgentId, activeSessionId } = chat + const { apiServer } = useSettings() const mainRef = React.useRef(null) const contentSearchRef = React.useRef(null) @@ -149,8 +151,15 @@ const Chat: FC = (props) => { if (!sessionId) { return () =>
Active Session ID is invalid.
} + if (!apiServer.enabled) { + return () => ( +
+ +
+ ) + } return () => - }, [activeAgentId, activeSessionId]) + }, [activeAgentId, activeSessionId, apiServer.enabled, t]) const SessionInputBar = useMemo(() => { if (activeAgentId === null) { diff --git a/src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx b/src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx index 000337152f..618d70e668 100644 --- a/src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx @@ -1,21 +1,28 @@ +import { Tooltip } from '@heroui/react' import { loggerService } from '@logger' +import { ActionIconButton } from '@renderer/components/Buttons' import { QuickPanelView } from '@renderer/components/QuickPanel' import { useSession } from '@renderer/hooks/agents/useSession' +import { selectNewTopicLoading } from '@renderer/hooks/useMessageOperations' import { getModel } from '@renderer/hooks/useModel' import { useSettings } from '@renderer/hooks/useSettings' import { useTimer } from '@renderer/hooks/useTimer' import PasteService from '@renderer/services/PasteService' -import { useAppDispatch } from '@renderer/store' +import { pauseTrace } from '@renderer/services/SpanManagerService' +import { useAppDispatch, useAppSelector } from '@renderer/store' +import { newMessagesActions, selectMessagesForTopic } from '@renderer/store/newMessage' import { sendMessage as dispatchSendMessage } from '@renderer/store/thunk/messageThunk' import type { Assistant, Message, Model, Topic } from '@renderer/types' import { MessageBlock, MessageBlockStatus } from '@renderer/types/newMessage' import { classNames } from '@renderer/utils' +import { abortCompletion } from '@renderer/utils/abortController' import { buildAgentSessionTopicId } from '@renderer/utils/agentSession' import { getSendMessageShortcutLabel, isSendMessageKeyPressed } from '@renderer/utils/input' import { createMainTextBlock, createMessage } from '@renderer/utils/messageUtils/create' import TextArea, { TextAreaRef } from 'antd/es/input/TextArea' import { isEmpty } from 'lodash' -import React, { CSSProperties, FC, useCallback, useEffect, useRef, useState } from 'react' +import { CirclePause } from 'lucide-react' +import React, { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' import { v4 as uuid } from 'uuid' @@ -36,6 +43,7 @@ const AgentSessionInputbar: FC = ({ agentId, sessionId }) => { const [text, setText] = useState(_text) const [inputFocus, setInputFocus] = useState(false) const { session } = useSession(agentId, sessionId) + const { apiServer } = useSettings() const { sendMessageShortcut, fontSize, enableSpellCheck } = useSettings() const textareaRef = useRef(null) @@ -46,12 +54,37 @@ const AgentSessionInputbar: FC = ({ agentId, sessionId }) => { const { setTimeoutTimer } = useTimer() const dispatch = useAppDispatch() const sessionTopicId = buildAgentSessionTopicId(sessionId) + const topicMessages = useAppSelector((state) => selectMessagesForTopic(state, sessionTopicId)) + const loading = useAppSelector((state) => selectNewTopicLoading(state, sessionTopicId)) const focusTextarea = useCallback(() => { textareaRef.current?.focus() }, []) const inputEmpty = isEmpty(text) + const sendDisabled = inputEmpty || !apiServer.enabled + + const streamingAskIds = useMemo(() => { + if (!topicMessages) { + return [] + } + + const askIdSet = new Set() + for (const message of topicMessages) { + if (!message) continue + if (message.status === 'processing' || message.status === 'pending') { + if (message.askId) { + askIdSet.add(message.askId) + } else if (message.id) { + askIdSet.add(message.id) + } + } + } + + return Array.from(askIdSet) + }, [topicMessages]) + + const canAbort = loading && streamingAskIds.length > 0 const handleKeyDown = (event: React.KeyboardEvent) => { //to check if the SendMessage key is pressed @@ -95,8 +128,27 @@ const AgentSessionInputbar: FC = ({ agentId, sessionId }) => { } } + const abortAgentSession = useCallback(async () => { + if (!streamingAskIds.length) { + logger.debug('No active agent session streams to abort', { sessionTopicId }) + return + } + + logger.info('Aborting agent session message generation', { + sessionTopicId, + askIds: streamingAskIds + }) + + for (const askId of streamingAskIds) { + abortCompletion(askId) + } + + pauseTrace(sessionTopicId) + dispatch(newMessagesActions.setTopicLoading({ topicId: sessionTopicId, loading: false })) + }, [dispatch, sessionTopicId, streamingAskIds]) + const sendMessage = useCallback(async () => { - if (inputEmpty) { + if (sendDisabled) { return } @@ -158,7 +210,7 @@ const AgentSessionInputbar: FC = ({ agentId, sessionId }) => { }, [ agentId, dispatch, - inputEmpty, + sendDisabled, session?.agent_id, session?.instructions, session?.model, @@ -231,7 +283,16 @@ const AgentSessionInputbar: FC = ({ agentId, sessionId }) => { onBlur={() => setInputFocus(false)} />
- +
+ + {canAbort && ( + + + + + + )} +
diff --git a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx index 9299a75e2f..0fef9a968f 100644 --- a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx @@ -3,7 +3,7 @@ import { Assistant } from '@renderer/types' import { FC, useRef } from 'react' import styled from 'styled-components' -import { Agents } from './components/Agents' +import { AgentSection } from './components/AgentSection' import Assistants from './components/Assistants' interface AssistantsTabProps { @@ -17,7 +17,7 @@ const AssistantsTab: FC = (props) => { const containerRef = useRef(null) return ( - + ) diff --git a/src/renderer/src/pages/home/Tabs/SessionsTab.tsx b/src/renderer/src/pages/home/Tabs/SessionsTab.tsx index eb0d994d6c..fd75efc1e3 100644 --- a/src/renderer/src/pages/home/Tabs/SessionsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/SessionsTab.tsx @@ -1,5 +1,6 @@ -import { Spinner } from '@heroui/react' +import { Alert, Spinner } from '@heroui/react' import { useRuntime } from '@renderer/hooks/useRuntime' +import { useSettings } from '@renderer/hooks/useSettings' import { AnimatePresence, motion } from 'framer-motion' import { FC, memo } from 'react' import { useTranslation } from 'react-i18next' @@ -12,6 +13,15 @@ const SessionsTab: FC = () => { const { chat } = useRuntime() const { activeAgentId } = chat const { t } = useTranslation() + const { apiServer } = useSettings() + + if (!apiServer.enabled) { + return ( +
+ +
+ ) + } return ( diff --git a/src/renderer/src/pages/home/Tabs/components/AgentSection.tsx b/src/renderer/src/pages/home/Tabs/components/AgentSection.tsx new file mode 100644 index 0000000000..06eb072219 --- /dev/null +++ b/src/renderer/src/pages/home/Tabs/components/AgentSection.tsx @@ -0,0 +1,39 @@ +import { Alert } from '@heroui/react' +import { useRuntime } from '@renderer/hooks/useRuntime' +import { useSettings } from '@renderer/hooks/useSettings' +import { useAppDispatch } from '@renderer/store' +import { addIknowAction } from '@renderer/store/runtime' +import { useTranslation } from 'react-i18next' + +import { Agents } from './Agents' +import { SectionName } from './SectionName' + +const ALERT_KEY = 'enable_api_server_to_use_agent' + +export const AgentSection = () => { + const { t } = useTranslation() + const { apiServer } = useSettings() + const { iknow } = useRuntime() + const dispatch = useAppDispatch() + + if (!apiServer.enabled) { + if (iknow[ALERT_KEY]) return null + return ( + { + dispatch(addIknowAction(ALERT_KEY)) + }} + /> + ) + } + + return ( +
+ + +
+ ) +} diff --git a/src/renderer/src/pages/home/Tabs/components/Agents.tsx b/src/renderer/src/pages/home/Tabs/components/Agents.tsx index d6ef7b2c24..253f0888a0 100644 --- a/src/renderer/src/pages/home/Tabs/components/Agents.tsx +++ b/src/renderer/src/pages/home/Tabs/components/Agents.tsx @@ -9,7 +9,6 @@ import { FC, useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' import AgentItem from './AgentItem' -import { SectionName } from './SectionName' interface AssistantsTabProps {} @@ -35,8 +34,7 @@ export const Agents: FC = () => { }, [isLoading, agents, activeAgentId, setActiveAgentId]) return ( -
- + <> {isLoading && } {error && } {!isLoading && @@ -65,6 +63,6 @@ export const Agents: FC = () => { ) }} /> -
+ ) } diff --git a/src/renderer/src/pages/home/Tabs/components/SessionItem.tsx b/src/renderer/src/pages/home/Tabs/components/SessionItem.tsx index 9d49627f1f..e51d485dd4 100644 --- a/src/renderer/src/pages/home/Tabs/components/SessionItem.tsx +++ b/src/renderer/src/pages/home/Tabs/components/SessionItem.tsx @@ -13,11 +13,13 @@ interface SessionItemProps { session: AgentSessionEntity // use external agentId as SSOT, instead of session.agent_id agentId: string + isDisabled?: boolean + isLoading?: boolean onDelete: () => void onPress: () => void } -const SessionItem: FC = ({ session, agentId, onDelete, onPress }) => { +const SessionItem: FC = ({ session, agentId, isDisabled, isLoading, onDelete, onPress }) => { const { t } = useTranslation() const { isOpen, onOpen, onClose } = useDisclosure() const { chat } = useRuntime() @@ -38,7 +40,11 @@ const SessionItem: FC = ({ session, agentId, onDelete, onPress <> - + diff --git a/src/renderer/src/pages/home/Tabs/components/Sessions.tsx b/src/renderer/src/pages/home/Tabs/components/Sessions.tsx index a8ccab7d00..dc2263661a 100644 --- a/src/renderer/src/pages/home/Tabs/components/Sessions.tsx +++ b/src/renderer/src/pages/home/Tabs/components/Sessions.tsx @@ -1,9 +1,15 @@ -import { Button, Spinner } from '@heroui/react' -import { SessionModal } from '@renderer/components/Popups/agent/SessionModal' +import { Alert, Button, Spinner } from '@heroui/react' +import { useAgent } from '@renderer/hooks/agents/useAgent' import { useSessions } from '@renderer/hooks/agents/useSessions' +import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession' import { useRuntime } from '@renderer/hooks/useRuntime' import { useAppDispatch } from '@renderer/store' -import { setActiveSessionIdAction, setActiveTopicOrSessionAction } from '@renderer/store/runtime' +import { + setActiveSessionIdAction, + setActiveTopicOrSessionAction, + setSessionWaitingAction +} from '@renderer/store/runtime' +import { CreateSessionForm } from '@renderer/types' import { AnimatePresence, motion } from 'framer-motion' import { Plus } from 'lucide-react' import { memo, useCallback, useEffect } from 'react' @@ -19,9 +25,11 @@ interface SessionsProps { const Sessions: React.FC = ({ agentId }) => { const { t } = useTranslation() - const { sessions, isLoading, deleteSession } = useSessions(agentId) + const { agent } = useAgent(agentId) + const { sessions, isLoading, error, deleteSession, createSession } = useSessions(agentId) + const updateSession = useUpdateSession(agentId) const { chat } = useRuntime() - const { activeSessionId } = chat + const { activeSessionId, sessionWaiting } = chat const dispatch = useAppDispatch() const setActiveSessionId = useCallback( @@ -32,6 +40,39 @@ const Sessions: React.FC = ({ agentId }) => { [dispatch] ) + const handleCreateSession = useCallback(async () => { + if (!agent) return + const session = { + ...agent, + id: undefined + } satisfies CreateSessionForm + const created = await createSession(session) + if (created) { + dispatch(setActiveSessionIdAction({ agentId, sessionId: created.id })) + } + }, [agent, agentId, createSession, dispatch]) + + const handleDeleteSession = useCallback( + async (id: string) => { + if (sessions.length === 1) { + window.toast.error(t('agent.session.delete.error.last')) + return + } + dispatch(setSessionWaitingAction({ id, value: true })) + const success = await deleteSession(id) + if (success) { + const newSessionId = sessions.find((s) => s.id !== id)?.id + if (newSessionId) { + dispatch(setActiveSessionIdAction({ agentId, sessionId: newSessionId })) + } else { + // may clear messages instead of forbidden deletion + } + } + dispatch(setSessionWaitingAction({ id, value: false })) + }, + [agentId, deleteSession, dispatch, sessions] + ) + const currentActiveSessionId = activeSessionId[agentId] useEffect(() => { @@ -52,7 +93,7 @@ const Sessions: React.FC = ({ agentId }) => { ) } - // if (error) return + if (error) return return ( = ({ agentId }) => { initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.2, delay: 0.1 }}> - setActiveSessionId(agentId, created.id)} - trigger={{ - content: ( - - ) - }} - /> + {sessions.map((session, index) => ( @@ -90,7 +123,9 @@ const Sessions: React.FC = ({ agentId }) => { deleteSession(session.id)} + isDisabled={sessionWaiting[session.id]} + isLoading={sessionWaiting[session.id]} + onDelete={() => handleDeleteSession(session.id)} onPress={() => setActiveSessionId(agentId, session.id)} /> diff --git a/src/renderer/src/pages/home/Tabs/components/SessionsSection.tsx b/src/renderer/src/pages/home/Tabs/components/SessionsSection.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/renderer/src/store/runtime.ts b/src/renderer/src/store/runtime.ts index fafee81808..4c0183e9e4 100644 --- a/src/renderer/src/store/runtime.ts +++ b/src/renderer/src/store/runtime.ts @@ -18,6 +18,8 @@ export interface ChatState { renamingTopics: string[] /** topic ids that are newly renamed */ newlyRenamedTopics: string[] + /** is a session waiting for updating/deleting. undefined and false share same semantics. */ + sessionWaiting: Record } export interface WebSearchState { @@ -53,6 +55,7 @@ export interface RuntimeState { export: ExportState chat: ChatState websearch: WebSearchState + iknow: Record } export interface ExportState { @@ -89,11 +92,13 @@ const initialState: RuntimeState = { activeTopicOrSession: 'topic', activeSessionId: {}, renamingTopics: [], - newlyRenamedTopics: [] + newlyRenamedTopics: [], + sessionWaiting: {} }, websearch: { activeSearches: {} - } + }, + iknow: {} } const runtimeSlice = createSlice({ @@ -179,6 +184,13 @@ const runtimeSlice = createSlice({ delete state.websearch.activeSearches[requestId] } state.websearch.activeSearches[requestId] = status + }, + addIknowAction: (state, action: PayloadAction) => { + state.iknow[action.payload] = true + }, + setSessionWaitingAction: (state, action: PayloadAction<{ id: string; value: boolean }>) => { + const { id, value } = action.payload + state.chat.sessionWaiting[id] = value } } }) @@ -197,6 +209,7 @@ export const { setResourcesPath, setUpdateState, setExportState, + addIknowAction, // Chat related actions toggleMultiSelectMode, setSelectedMessageIds, @@ -206,6 +219,7 @@ export const { setActiveTopicOrSessionAction, setRenamingTopics, setNewlyRenamedTopics, + setSessionWaitingAction, // WebSearch related actions setActiveSearches, setWebSearchStatus