diff --git a/frontend/src/features/tasks/components/chat/ChatArea.tsx b/frontend/src/features/tasks/components/chat/ChatArea.tsx index 91c1832c7..9e9373fed 100644 --- a/frontend/src/features/tasks/components/chat/ChatArea.tsx +++ b/frontend/src/features/tasks/components/chat/ChatArea.tsx @@ -116,7 +116,7 @@ function ChatAreaContent({ const { quote, clearQuote, formatQuoteForMessage } = useQuote() // Task context - const { selectedTaskDetail, setSelectedTask, accessDenied } = useTaskContext() + const { selectedTask, selectedTaskDetail, setSelectedTask, accessDenied } = useTaskContext() // Use useTaskStateMachine hook for reactive state updates (SINGLE SOURCE OF TRUTH per AGENTS.md) const { state: taskState } = useTaskStateMachine(selectedTaskDetail?.id) @@ -313,12 +313,14 @@ function ChatAreaContent({ useEffect(() => { if (filteredTeams.length === 0) return - // Extract team ID from task detail + // Extract team ID from task detail or selectedTask as fallback + // selectedTask is set immediately when clicking a task from history + // selectedTaskDetail may take time to load and may not include team info const detailTeamId = selectedTaskDetail?.team ? typeof selectedTaskDetail.team === 'number' ? selectedTaskDetail.team : (selectedTaskDetail.team as Team).id - : null + : selectedTask?.team_id || null // Case 1: Sync from task detail (HIGHEST PRIORITY) // Only sync when URL taskId matches taskDetail.id to prevent race conditions @@ -389,6 +391,7 @@ function ChatAreaContent({ } }, [ filteredTeams, + selectedTask, selectedTaskDetail, taskIdFromUrl, selectedTeam, diff --git a/frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx b/frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx index da224381b..2b5b7eae1 100644 --- a/frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx +++ b/frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx @@ -452,6 +452,16 @@ export function useChatStreamHandlers({ const message = overrideMessage?.trim() || taskInputMessage.trim() if (!message && !shouldHideChatInput) return + // Check if team is selected before sending + if (!selectedTeam?.id) { + toast({ + variant: 'destructive', + title: t('chat:errors.team_not_selected') || '请选择智能体', + description: t('chat:errors.team_not_selected_description') || '请从列表中选择一个智能体后开始对话', + }) + return + } + if (!isAttachmentReadyToSend) { toast({ variant: 'destructive', diff --git a/frontend/src/features/tasks/contexts/taskContext.tsx b/frontend/src/features/tasks/contexts/taskContext.tsx index a7c9a24f5..9fa588512 100644 --- a/frontend/src/features/tasks/contexts/taskContext.tsx +++ b/frontend/src/features/tasks/contexts/taskContext.tsx @@ -711,10 +711,9 @@ export const TaskContextProvider = ({ children }: { children: ReactNode }) => { } } - // Trigger task detail refresh and manage WebSocket room when selectedTask changes - // NOTE: joinTask is called here, and SocketContext's joinTask already has deduplication logic - // (joinedTasksRef.current.has(taskId) check), so multiple calls are safe but wasteful. - // We only call joinTask when isConnected is true to avoid unnecessary calls. + // Effect 1: Handle task selection changes + // When selectedTask changes, leave old room and join new room + // This effect only reacts to selectedTask changes, not isConnected useEffect(() => { const currentTaskId = selectedTask?.id ?? null const previousTaskId = previousTaskIdRef.current @@ -728,8 +727,8 @@ export const TaskContextProvider = ({ children }: { children: ReactNode }) => { previousTaskIdRef.current = currentTaskId if (selectedTask) { - // Only join task room when WebSocket is connected - // This prevents duplicate joins when both selectedTask and isConnected change + // Join task room when WebSocket is connected + // joinTask has built-in deduplication, so it's safe to call multiple times if (isConnected) { joinTask(selectedTask.id) } @@ -738,14 +737,22 @@ export const TaskContextProvider = ({ children }: { children: ReactNode }) => { } else { setSelectedTaskDetail(null) } + // Note: isConnected is intentionally NOT in dependencies - reconnect logic is handled by Effect 2 // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedTask, leaveTask, joinTask, isConnected]) + }, [selectedTask, leaveTask, joinTask]) - // NOTE: Removed separate isConnected useEffect to prevent duplicate joinTask calls. - // The selectedTask useEffect above now handles both cases: - // 1. When selectedTask changes (and isConnected is true) - // 2. When isConnected changes (and selectedTask is set) - // This is because we added isConnected to the dependency array. + // Effect 2: Handle WebSocket reconnection + // When WebSocket reconnects (isConnected changes from false to true), + // rejoin the current task room to resume real-time updates + useEffect(() => { + // Only proceed if WebSocket is connected and there is a selected task + if (isConnected && selectedTask) { + // Rejoin the task room to ensure we receive real-time updates + // joinTask has built-in deduplication (joinedTasksRef), so it's safe to call again + joinTask(selectedTask.id) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isConnected]) // Mark task as viewed when selectedTaskDetail is loaded // This ensures we have the correct status and timestamps from the backend diff --git a/frontend/src/i18n/locales/en/chat.json b/frontend/src/i18n/locales/en/chat.json index 5d7587df5..eb179493a 100644 --- a/frontend/src/i18n/locales/en/chat.json +++ b/frontend/src/i18n/locales/en/chat.json @@ -257,7 +257,9 @@ "container_oom": "Executor out of memory: Please start a new conversation to retry, or contact administrator to increase memory allocation", "container_error": "Executor error: Container has stopped running. Please start a new conversation to retry", "generic_error": "Request failed: Unknown error, please try again or contact administrator", - "copy_error": "Copy error details" + "copy_error": "Copy error details", + "team_not_selected": "No Agent Selected", + "team_not_selected_description": "Please select an agent from the list to start a conversation" }, "clarification": { "title": "Smart Follow-up", diff --git a/frontend/src/i18n/locales/zh-CN/chat.json b/frontend/src/i18n/locales/zh-CN/chat.json index cb7982753..3b99052c1 100644 --- a/frontend/src/i18n/locales/zh-CN/chat.json +++ b/frontend/src/i18n/locales/zh-CN/chat.json @@ -235,7 +235,9 @@ "container_oom": "执行环境内存不足:请新建一个对话任务重试,或联系管理员增加内存配置", "container_error": "执行环境异常:容器已停止运行,请新建一个对话任务重试", "generic_error": "请求失败:未知错误,请重试或联系管理员", - "copy_error": "复制错误详情" + "copy_error": "复制错误详情", + "team_not_selected": "未选择智能体", + "team_not_selected_description": "请从列表中选择一个智能体后开始对话" }, "clarification": { "title": "智能追问 (Smart Follow-up)",