mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-09 14:59:27 +08:00
feat: allow new-topic bindkey to create new session for agent as well (#10862)
* fix: allow new-topic shortcut to create agent sessions * revert: restore ProxyManager.ts * fix: make agent session shortcut sidebar-independent * refactor: centralize default session creation * feat: add new session button to agent inputbar * fix: encapsulate agent session creation state * refactor: remove redundant useMemo in useCreateDefaultSession The useMemo wrapper around the return object was unnecessary because: - createDefaultSession is already memoized via useCallback - creatingSession is a primitive boolean that doesn't need memoization - The object gets recreated on every creatingSession change anyway This simplifies the code and removes unnecessary overhead. --------- Co-authored-by: wangdenghui <wangdenghui@xiaomi.com>
This commit is contained in:
parent
5986800c9d
commit
9a01e092f6
49
src/renderer/src/hooks/agents/useCreateDefaultSession.ts
Normal file
49
src/renderer/src/hooks/agents/useCreateDefaultSession.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||||
|
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||||
|
import { useAppDispatch } from '@renderer/store'
|
||||||
|
import { setActiveSessionIdAction, setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
||||||
|
import type { CreateSessionForm } from '@renderer/types'
|
||||||
|
import { useCallback, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a stable callback that creates a default agent session and updates UI state.
|
||||||
|
*/
|
||||||
|
export const useCreateDefaultSession = (agentId: string | null) => {
|
||||||
|
const { agent } = useAgent(agentId)
|
||||||
|
const { createSession } = useSessions(agentId)
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [creatingSession, setCreatingSession] = useState(false)
|
||||||
|
|
||||||
|
const createDefaultSession = useCallback(async () => {
|
||||||
|
if (!agentId || !agent || creatingSession) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
setCreatingSession(true)
|
||||||
|
try {
|
||||||
|
const session = {
|
||||||
|
...agent,
|
||||||
|
id: undefined,
|
||||||
|
name: t('common.unnamed')
|
||||||
|
} satisfies CreateSessionForm
|
||||||
|
|
||||||
|
const created = await createSession(session)
|
||||||
|
|
||||||
|
if (created) {
|
||||||
|
dispatch(setActiveSessionIdAction({ agentId, sessionId: created.id }))
|
||||||
|
dispatch(setActiveTopicOrSessionAction('session'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return created
|
||||||
|
} finally {
|
||||||
|
setCreatingSession(false)
|
||||||
|
}
|
||||||
|
}, [agentId, agent, createSession, creatingSession, dispatch, t])
|
||||||
|
|
||||||
|
return {
|
||||||
|
createDefaultSession,
|
||||||
|
creatingSession
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import { HStack } from '@renderer/components/Layout'
|
|||||||
import MultiSelectActionPopup from '@renderer/components/Popups/MultiSelectionPopup'
|
import MultiSelectActionPopup from '@renderer/components/Popups/MultiSelectionPopup'
|
||||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||||
import { QuickPanelProvider } from '@renderer/components/QuickPanel'
|
import { QuickPanelProvider } from '@renderer/components/QuickPanel'
|
||||||
|
import { useCreateDefaultSession } from '@renderer/hooks/agents/useCreateDefaultSession'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
@ -52,6 +53,8 @@ const Chat: FC<Props> = (props) => {
|
|||||||
const { activeTopicOrSession, activeAgentId, activeSessionIdMap } = chat
|
const { activeTopicOrSession, activeAgentId, activeSessionIdMap } = chat
|
||||||
const activeSessionId = activeAgentId ? activeSessionIdMap[activeAgentId] : null
|
const activeSessionId = activeAgentId ? activeSessionIdMap[activeAgentId] : null
|
||||||
const { apiServer } = useSettings()
|
const { apiServer } = useSettings()
|
||||||
|
const sessionAgentId = activeTopicOrSession === 'session' ? activeAgentId : null
|
||||||
|
const { createDefaultSession } = useCreateDefaultSession(sessionAgentId)
|
||||||
|
|
||||||
const mainRef = React.useRef<HTMLDivElement>(null)
|
const mainRef = React.useRef<HTMLDivElement>(null)
|
||||||
const contentSearchRef = React.useRef<ContentSearchRef>(null)
|
const contentSearchRef = React.useRef<ContentSearchRef>(null)
|
||||||
@ -90,6 +93,21 @@ const Chat: FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useShortcut(
|
||||||
|
'new_topic',
|
||||||
|
() => {
|
||||||
|
if (activeTopicOrSession !== 'session' || !activeAgentId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
void createDefaultSession()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: activeTopicOrSession === 'session',
|
||||||
|
preventDefault: true,
|
||||||
|
enableOnFormTags: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const contentSearchFilter: NodeFilter = {
|
const contentSearchFilter: NodeFilter = {
|
||||||
acceptNode(node) {
|
acceptNode(node) {
|
||||||
const container = node.parentElement?.closest('.message-content-container')
|
const container = node.parentElement?.closest('.message-content-container')
|
||||||
|
|||||||
@ -3,10 +3,12 @@ import { loggerService } from '@logger'
|
|||||||
import { ActionIconButton } from '@renderer/components/Buttons'
|
import { ActionIconButton } from '@renderer/components/Buttons'
|
||||||
import { QuickPanelView } from '@renderer/components/QuickPanel'
|
import { QuickPanelView } from '@renderer/components/QuickPanel'
|
||||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||||
|
import { useCreateDefaultSession } from '@renderer/hooks/agents/useCreateDefaultSession'
|
||||||
import { useSession } from '@renderer/hooks/agents/useSession'
|
import { useSession } from '@renderer/hooks/agents/useSession'
|
||||||
import { selectNewTopicLoading } from '@renderer/hooks/useMessageOperations'
|
import { selectNewTopicLoading } from '@renderer/hooks/useMessageOperations'
|
||||||
import { getModel } from '@renderer/hooks/useModel'
|
import { getModel } from '@renderer/hooks/useModel'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { useShortcutDisplay } from '@renderer/hooks/useShortcuts'
|
||||||
import { useTimer } from '@renderer/hooks/useTimer'
|
import { useTimer } from '@renderer/hooks/useTimer'
|
||||||
import PasteService from '@renderer/services/PasteService'
|
import PasteService from '@renderer/services/PasteService'
|
||||||
import { pauseTrace } from '@renderer/services/SpanManagerService'
|
import { pauseTrace } from '@renderer/services/SpanManagerService'
|
||||||
@ -22,7 +24,7 @@ import { getSendMessageShortcutLabel, isSendMessageKeyPressed } from '@renderer/
|
|||||||
import { createMainTextBlock, createMessage } from '@renderer/utils/messageUtils/create'
|
import { createMainTextBlock, createMessage } from '@renderer/utils/messageUtils/create'
|
||||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { CirclePause } from 'lucide-react'
|
import { CirclePause, MessageSquareDiff } from 'lucide-react'
|
||||||
import React, { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@ -46,6 +48,8 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
|||||||
const { session } = useSession(agentId, sessionId)
|
const { session } = useSession(agentId, sessionId)
|
||||||
const { agent } = useAgent(agentId)
|
const { agent } = useAgent(agentId)
|
||||||
const { apiServer } = useSettings()
|
const { apiServer } = useSettings()
|
||||||
|
const { createDefaultSession, creatingSession } = useCreateDefaultSession(agentId)
|
||||||
|
const newTopicShortcut = useShortcutDisplay('new_topic')
|
||||||
|
|
||||||
const { sendMessageShortcut, fontSize, enableSpellCheck } = useSettings()
|
const { sendMessageShortcut, fontSize, enableSpellCheck } = useSettings()
|
||||||
const textareaRef = useRef<TextAreaRef>(null)
|
const textareaRef = useRef<TextAreaRef>(null)
|
||||||
@ -87,6 +91,22 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
|||||||
}, [topicMessages])
|
}, [topicMessages])
|
||||||
|
|
||||||
const canAbort = loading && streamingAskIds.length > 0
|
const canAbort = loading && streamingAskIds.length > 0
|
||||||
|
const createSessionDisabled = creatingSession || !apiServer.enabled
|
||||||
|
|
||||||
|
const handleCreateSession = useCallback(async () => {
|
||||||
|
if (createSessionDisabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const created = await createDefaultSession()
|
||||||
|
if (created) {
|
||||||
|
focusTextarea()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Failed to create agent session via toolbar:', error as Error)
|
||||||
|
}
|
||||||
|
}, [createDefaultSession, createSessionDisabled, focusTextarea])
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
//to check if the SendMessage key is pressed
|
//to check if the SendMessage key is pressed
|
||||||
@ -286,8 +306,18 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
|||||||
}}
|
}}
|
||||||
onBlur={() => setInputFocus(false)}
|
onBlur={() => setInputFocus(false)}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end px-1">
|
<Toolbar>
|
||||||
<div className="flex items-center gap-1">
|
<ToolbarGroup>
|
||||||
|
<Tooltip placement="top" content={t('chat.input.new_topic', { Command: newTopicShortcut })} delay={0}>
|
||||||
|
<ActionIconButton
|
||||||
|
onClick={handleCreateSession}
|
||||||
|
disabled={createSessionDisabled}
|
||||||
|
loading={creatingSession}>
|
||||||
|
<MessageSquareDiff size={19} />
|
||||||
|
</ActionIconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</ToolbarGroup>
|
||||||
|
<ToolbarGroup>
|
||||||
<SendMessageButton sendMessage={sendMessage} disabled={sendDisabled} />
|
<SendMessageButton sendMessage={sendMessage} disabled={sendDisabled} />
|
||||||
{canAbort && (
|
{canAbort && (
|
||||||
<Tooltip placement="top" content={t('chat.input.pause')}>
|
<Tooltip placement="top" content={t('chat.input.pause')}>
|
||||||
@ -296,8 +326,8 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
|||||||
</ActionIconButton>
|
</ActionIconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</ToolbarGroup>
|
||||||
</div>
|
</Toolbar>
|
||||||
</InputBarContainer>
|
</InputBarContainer>
|
||||||
</Container>
|
</Container>
|
||||||
</NarrowLayout>
|
</NarrowLayout>
|
||||||
@ -343,6 +373,25 @@ const InputBarContainer = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const Toolbar = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 5px 8px;
|
||||||
|
height: 40px;
|
||||||
|
gap: 16px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
flex-shrink: 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ToolbarGroup = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
`
|
||||||
|
|
||||||
const TextareaStyle: CSSProperties = {
|
const TextareaStyle: CSSProperties = {
|
||||||
paddingLeft: 0,
|
paddingLeft: 0,
|
||||||
padding: '6px 15px 0px' // 减小顶部padding
|
padding: '6px 15px 0px' // 减小顶部padding
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Alert, Spinner } from '@heroui/react'
|
import { Alert, Spinner } from '@heroui/react'
|
||||||
import { DynamicVirtualList } from '@renderer/components/VirtualList'
|
import { DynamicVirtualList } from '@renderer/components/VirtualList'
|
||||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
import { useCreateDefaultSession } from '@renderer/hooks/agents/useCreateDefaultSession'
|
||||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
@ -10,7 +10,6 @@ import {
|
|||||||
setActiveTopicOrSessionAction,
|
setActiveTopicOrSessionAction,
|
||||||
setSessionWaitingAction
|
setSessionWaitingAction
|
||||||
} from '@renderer/store/runtime'
|
} from '@renderer/store/runtime'
|
||||||
import { CreateSessionForm } from '@renderer/types'
|
|
||||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { memo, useCallback, useEffect } from 'react'
|
import { memo, useCallback, useEffect } from 'react'
|
||||||
@ -27,11 +26,11 @@ interface SessionsProps {
|
|||||||
|
|
||||||
const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { agent } = useAgent(agentId)
|
const { sessions, isLoading, error, deleteSession } = useSessions(agentId)
|
||||||
const { sessions, isLoading, error, deleteSession, createSession } = useSessions(agentId)
|
|
||||||
const { chat } = useRuntime()
|
const { chat } = useRuntime()
|
||||||
const { activeSessionIdMap } = chat
|
const { activeSessionIdMap } = chat
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const { createDefaultSession, creatingSession } = useCreateDefaultSession(agentId)
|
||||||
|
|
||||||
const setActiveSessionId = useCallback(
|
const setActiveSessionId = useCallback(
|
||||||
(agentId: string, sessionId: string | null) => {
|
(agentId: string, sessionId: string | null) => {
|
||||||
@ -41,19 +40,6 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
|||||||
[dispatch]
|
[dispatch]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleCreateSession = useCallback(async () => {
|
|
||||||
if (!agent) return
|
|
||||||
const session = {
|
|
||||||
...agent,
|
|
||||||
id: undefined,
|
|
||||||
name: t('common.unnamed')
|
|
||||||
} satisfies CreateSessionForm
|
|
||||||
const created = await createSession(session)
|
|
||||||
if (created) {
|
|
||||||
dispatch(setActiveSessionIdAction({ agentId, sessionId: created.id }))
|
|
||||||
}
|
|
||||||
}, [agent, agentId, createSession, dispatch, t])
|
|
||||||
|
|
||||||
const handleDeleteSession = useCallback(
|
const handleDeleteSession = useCallback(
|
||||||
async (id: string) => {
|
async (id: string) => {
|
||||||
if (sessions.length === 1) {
|
if (sessions.length === 1) {
|
||||||
@ -110,7 +96,7 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sessions-tab flex h-full w-full flex-col p-2">
|
<div className="sessions-tab flex h-full w-full flex-col p-2">
|
||||||
<AddButton onPress={handleCreateSession} className="mb-2">
|
<AddButton onPress={createDefaultSession} className="mb-2" isDisabled={creatingSession}>
|
||||||
{t('agent.session.add.title')}
|
{t('agent.session.add.title')}
|
||||||
</AddButton>
|
</AddButton>
|
||||||
{/* h-9 */}
|
{/* h-9 */}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user