From 42dbc6555c34017101d267c30eca8b1a508218e9 Mon Sep 17 00:00:00 2001 From: icarus Date: Mon, 22 Sep 2025 17:55:21 +0800 Subject: [PATCH 1/4] feat(sessions): make session creation async and set active session Dispatch active session id after successful creation to ensure UI reflects current state --- src/renderer/src/pages/home/Tabs/components/Sessions.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/pages/home/Tabs/components/Sessions.tsx b/src/renderer/src/pages/home/Tabs/components/Sessions.tsx index f3bfed18d6..455e32aeca 100644 --- a/src/renderer/src/pages/home/Tabs/components/Sessions.tsx +++ b/src/renderer/src/pages/home/Tabs/components/Sessions.tsx @@ -34,14 +34,17 @@ const Sessions: React.FC = ({ agentId }) => { [dispatch] ) - const handleCreateSession = useCallback(() => { + const handleCreateSession = useCallback(async () => { if (!agent) return const session = { ...agent, id: undefined } satisfies CreateSessionForm - createSession(session) - }, [agent, createSession]) + const created = await createSession(session) + if (created) { + dispatch(setActiveSessionIdAction({ agentId, sessionId: created.id })) + } + }, [agent, agentId, createSession, dispatch]) const currentActiveSessionId = activeSessionId[agentId] From 52f00f08f21869463d597eba9b0cd536f34f108d Mon Sep 17 00:00:00 2001 From: icarus Date: Mon, 22 Sep 2025 18:47:24 +0800 Subject: [PATCH 2/4] feat(sessions): add session waiting state and improve deletion handling - Add sessionWaiting state to track updating/deleting sessions - Extract updateSession logic into separate hook - Improve session deletion with waiting state and fallback session selection - Disable session items during deletion to prevent duplicate actions --- .../components/Popups/agent/SessionModal.tsx | 6 ++-- src/renderer/src/hooks/agents/useSessions.ts | 22 +++--------- .../src/hooks/agents/useUpdateSession.ts | 36 +++++++++++++++++++ .../home/Tabs/components/SessionItem.tsx | 10 ++++-- .../pages/home/Tabs/components/Sessions.tsx | 35 ++++++++++++++++-- src/renderer/src/store/runtime.ts | 10 +++++- 6 files changed, 94 insertions(+), 25 deletions(-) create mode 100644 src/renderer/src/hooks/agents/useUpdateSession.ts 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/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 455e32aeca..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,14 @@ 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' @@ -22,8 +27,9 @@ const Sessions: React.FC = ({ agentId }) => { const { t } = useTranslation() 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( @@ -46,6 +52,27 @@ const Sessions: React.FC = ({ agentId }) => { } }, [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(() => { @@ -96,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/store/runtime.ts b/src/renderer/src/store/runtime.ts index 99dcc1914f..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 { @@ -90,7 +92,8 @@ const initialState: RuntimeState = { activeTopicOrSession: 'topic', activeSessionId: {}, renamingTopics: [], - newlyRenamedTopics: [] + newlyRenamedTopics: [], + sessionWaiting: {} }, websearch: { activeSearches: {} @@ -184,6 +187,10 @@ const runtimeSlice = createSlice({ }, 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 } } }) @@ -212,6 +219,7 @@ export const { setActiveTopicOrSessionAction, setRenamingTopics, setNewlyRenamedTopics, + setSessionWaitingAction, // WebSearch related actions setActiveSearches, setWebSearchStatus From 632871b2f89ef45f4da0c6a2139dd47ea5e224ee Mon Sep 17 00:00:00 2001 From: icarus Date: Mon, 22 Sep 2025 18:48:11 +0800 Subject: [PATCH 3/4] feat(i18n): add error message for last session deletion Add error message to prevent deletion of the last session in all supported languages --- src/renderer/src/i18n/locales/en-us.json | 3 ++- src/renderer/src/i18n/locales/zh-cn.json | 3 ++- src/renderer/src/i18n/locales/zh-tw.json | 3 ++- src/renderer/src/i18n/translate/el-gr.json | 3 ++- src/renderer/src/i18n/translate/es-es.json | 3 ++- src/renderer/src/i18n/translate/fr-fr.json | 3 ++- src/renderer/src/i18n/translate/ja-jp.json | 3 ++- src/renderer/src/i18n/translate/pt-pt.json | 3 ++- src/renderer/src/i18n/translate/ru-ru.json | 3 ++- 9 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 9aa40c3e0a..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" }, diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index a91e78959b..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": "删除会话" }, diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index ee56af1c9b..90f559a2fa 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": "[to be translated]:At least one session must be kept" }, "title": "刪除工作階段" }, diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index e18e8abb83..a82352dbf8 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": "[to be translated]:At least one session must be kept" }, "title": "Διαγραφή συνεδρίας" }, diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 3579018426..f8e074db67 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": "[to be translated]:At least one session must be kept" }, "title": "Eliminar sesión" }, diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index c1950340ae..9c9fab70bb 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": "[to be translated]:At least one session must be kept" }, "title": "Supprimer la session" }, diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 8450c73f59..3c09c2ea58 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": "[to be translated]:At least one session must be kept" }, "title": "セッションを削除" }, diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 9a6bb00ee2..5e6611daad 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": "[to be translated]:At least one session must be kept" }, "title": "Excluir sessão" }, diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 00b582f4a5..e84f6ca85c 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": "[to be translated]:At least one session must be kept" }, "title": "Удалить сеанс" }, From c9d1e30f8b5cb9b85262652c478db76b86e84dbc Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 22 Sep 2025 10:49:01 +0000 Subject: [PATCH 4/4] fix(i18n): Auto update translations for PR #10096 --- src/renderer/src/i18n/locales/zh-tw.json | 2 +- src/renderer/src/i18n/translate/el-gr.json | 2 +- src/renderer/src/i18n/translate/es-es.json | 2 +- src/renderer/src/i18n/translate/fr-fr.json | 2 +- src/renderer/src/i18n/translate/ja-jp.json | 2 +- src/renderer/src/i18n/translate/pt-pt.json | 2 +- src/renderer/src/i18n/translate/ru-ru.json | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index bf61c815f8..fbe922dbe3 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -53,7 +53,7 @@ "content": "您確定要刪除此工作階段嗎?", "error": { "failed": "無法刪除工作階段", - "last": "[to be translated]:At least one session must be kept" + "last": "至少必須保留一個工作階段" }, "title": "刪除工作階段" }, diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 627c820816..8af53b669c 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -53,7 +53,7 @@ "content": "Είσαι σίγουρος ότι θέλεις να διαγράψεις αυτήν τη συνεδρία;", "error": { "failed": "Αποτυχία διαγραφής της συνεδρίας", - "last": "[to be translated]:At least one session must be kept" + "last": "Τουλάχιστον μία συνεδρία πρέπει να διατηρηθεί" }, "title": "Διαγραφή συνεδρίας" }, diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 2e15e3cfe7..0be385d740 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -53,7 +53,7 @@ "content": "¿Estás seguro de eliminar esta sesión?", "error": { "failed": "Error al eliminar la sesión", - "last": "[to be translated]:At least one session must be kept" + "last": "Debe mantenerse al menos una sesión" }, "title": "Eliminar sesión" }, diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index d35be1a3c3..93209acb5a 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -53,7 +53,7 @@ "content": "Êtes-vous sûr de vouloir supprimer cette session ?", "error": { "failed": "Échec de la suppression de la session", - "last": "[to be translated]:At least one session must be kept" + "last": "Au moins une session doit être conservée" }, "title": "Supprimer la session" }, diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 7b0465d278..d23b5d15c9 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -53,7 +53,7 @@ "content": "このセッションを削除してもよろしいですか?", "error": { "failed": "セッションの削除に失敗しました", - "last": "[to be translated]:At least one session must be kept" + "last": "少なくとも1つのセッションを維持する必要があります" }, "title": "セッションを削除" }, diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 856b46b958..728347565c 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -53,7 +53,7 @@ "content": "Tem certeza de que deseja excluir esta sessão?", "error": { "failed": "Falha ao excluir a sessão", - "last": "[to be translated]:At least one session must be kept" + "last": "Pelo menos uma sessão deve ser mantida" }, "title": "Excluir sessão" }, diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 538fc7f1da..59ff59243a 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -53,7 +53,7 @@ "content": "Вы уверены, что хотите удалить этот сеанс?", "error": { "failed": "Не удалось удалить сеанс", - "last": "[to be translated]:At least one session must be kept" + "last": "Должен быть сохранён хотя бы один сеанс" }, "title": "Удалить сеанс" },