Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions frontend/src/features/tasks/components/chat/ChatArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ function ChatAreaContent({
const { quote, clearQuote, formatQuoteForMessage } = useQuote()

// Task context
const { selectedTaskDetail, setSelectedTask, accessDenied } = useTaskContext()
const { selectedTask, selectedTaskDetail, setSelectedTask, accessDenied } = useTaskContext()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use selectedTask.team_id before task detail resolves.

The fallback is computed, but Case 1 still requires selectedTaskDetail?.id, so a history click can leave selectedTeam unset/old until detail loading finishes. Sync from selectedTask when it matches the URL task ID.

Proposed fix
     const detailTeamId = selectedTaskDetail?.team
       ? typeof selectedTaskDetail.team === 'number'
         ? selectedTaskDetail.team
         : (selectedTaskDetail.team as Team).id
-      : selectedTask?.team_id || null
+      : null
+
+    const selectedTaskMatchesUrl =
+      taskIdFromUrl && selectedTask?.id.toString() === taskIdFromUrl
+
+    if (selectedTaskMatchesUrl && selectedTask.team_id) {
+      const teamFromTask = filteredTeams.find(t => t.id === selectedTask.team_id)
+      if (
+        teamFromTask &&
+        (lastSyncedTaskIdRef.current !== selectedTask.id ||
+          selectedTeam?.id !== selectedTask.team_id)
+      ) {
+        handleTeamChange(teamFromTask)
+        lastSyncedTaskIdRef.current = selectedTask.id
+        hasInitializedTeamRef.current = true
+        return
+      }
+    }
 
     // Case 1: Sync from task detail (HIGHEST PRIORITY)

Also applies to: 316-327, 392-400

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/tasks/components/chat/ChatArea.tsx` at line 119, The
bug is that selectedTeam can remain unset when a user clicks history because the
component waits for selectedTaskDetail?.id; update the selection logic in
ChatArea to fall back to selectedTask.team_id whenever selectedTask exists and
its id equals the task id from the URL (route param) so the team is synced
immediately while details load. Locate the places using selectedTaskDetail?.id
(e.g., the current fallback computation and the blocks around the earlier
mention and lines ~316-327 and ~392-400) and change them to check for
selectedTaskDetail?.id OR (selectedTask && selectedTask.id === urlTaskId &&
selectedTask.team_id) and use selectedTask.team_id to set
selectedTeam/setSelectedTask accordingly.


// Use useTaskStateMachine hook for reactive state updates (SINGLE SOURCE OF TRUTH per AGENTS.md)
const { state: taskState } = useTaskStateMachine(selectedTaskDetail?.id)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -389,6 +391,7 @@ function ChatAreaContent({
}
}, [
filteredTeams,
selectedTask,
selectedTaskDetail,
taskIdFromUrl,
selectedTeam,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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') || '请从列表中选择一个智能体后开始对话',
Comment on lines +459 to +460
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd -t f "useChatStreamHandlers.tsx"

Repository: wecode-ai/Wegent

Length of output: 129


🏁 Script executed:

head -n 850 frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx | tail -n +450 | head -n 50

Repository: wecode-ai/Wegent

Length of output: 1595


🏁 Script executed:

grep -n "handleSendMessage" frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx | head -20

Repository: wecode-ai/Wegent

Length of output: 728


🏁 Script executed:

# Get the full context around lines 459-460 and dependency array
sed -n '450,470p' frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx

Repository: wecode-ai/Wegent

Length of output: 736


🏁 Script executed:

# Look for the useCallback/useMemo hook that defines handleSendMessage and its dependencies
ast-grep --pattern 'const handleSendMessage = useCallback($$$, [$$$])'

Repository: wecode-ai/Wegent

Length of output: 40050


🏁 Script executed:

head -n 100 frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx | grep -E "import|useTranslation|useUnifiedMessages"

Repository: wecode-ai/Wegent

Length of output: 1284


🏁 Script executed:

# Search for all toast calls in the file to see if this is a pattern
grep -n "toast({" frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx | head -20

Repository: wecode-ai/Wegent

Length of output: 325


🏁 Script executed:

# Check if useUnifiedMessages is used elsewhere in this file
grep -n "useUnifiedMessages\|messages" frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx | head -20

Repository: wecode-ai/Wegent

Length of output: 685


🏁 Script executed:

# Get the full import statement for useTranslation
sed -n '1,50p' frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx | grep -A 5 "useTranslation"

Repository: wecode-ai/Wegent

Length of output: 422


🏁 Script executed:

# Check how t is used - find the destructuring or usage
grep -n "const.*=.*useTranslation\|const.*t.*=" frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx | head -5

Repository: wecode-ai/Wegent

Length of output: 248


🏁 Script executed:

# Search for where t is actually initialized in the hook
sed -n '100,200p' frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx

Repository: wecode-ai/Wegent

Length of output: 3029


🏁 Script executed:

# Verify if there are other useCallback hooks with t() usage in the file
grep -B 10 "t('chat:" frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx | grep -E "useCallback|t\(" | head -20

Repository: wecode-ai/Wegent

Length of output: 610


🏁 Script executed:

# Check the specific lines 764-804 to see the full dependency array
sed -n '764,804p' frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx

Repository: wecode-ai/Wegent

Length of output: 977


🏁 Script executed:

# Check if there are ESLint overrides or if react-hooks/exhaustive-deps would catch this
grep -r "exhaustive-deps\|eslint-disable" frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx

Repository: wecode-ai/Wegent

Length of output: 42


🏁 Script executed:

# Check other useCallback hooks to see if they also have the same issue with t
grep -B 5 -A 50 "const handleRetry = useCallback" frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx | grep -E "useCallback|t\(|^\s*\[" | head -30

Repository: wecode-ai/Wegent

Length of output: 299


🏁 Script executed:

# Check for eslint config and react-hooks/exhaustive-deps settings
fd -t f ".eslintrc*|eslint.config.*" frontend/

Repository: wecode-ai/Wegent

Length of output: 86


🏁 Script executed:

# Let's see if there's a package.json with eslint setup
head -n 100 package.json | grep -A 10 "eslint"

Repository: wecode-ai/Wegent

Length of output: 131


🏁 Script executed:

cat frontend/eslint.config.mjs

Repository: wecode-ai/Wegent

Length of output: 1016


🏁 Script executed:

# Look for handleSendMessageWithModel too, which was mentioned in the "Also applies to" comment
grep -B 5 -A 50 "const handleSendMessageWithModel = useCallback" frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx | head -80

Repository: wecode-ai/Wegent

Length of output: 2008


🏁 Script executed:

# Find the complete handleSendMessageWithModel dependency array
sed -n '814,1050p' frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx | grep -n -A 50 "^\s*\[" | head -60

Repository: wecode-ai/Wegent

Length of output: 2110


🏁 Script executed:

# Get the complete handleSendMessageWithModel dependency array more precisely
sed -n '1000,1050p' frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx

Repository: wecode-ai/Wegent

Length of output: 1189


🏁 Script executed:

# Find createRetryButton useCallback and its dependency array
grep -n "createRetryButton" frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx

Repository: wecode-ai/Wegent

Length of output: 205


🏁 Script executed:

# Get the context around createRetryButton
sed -n '390,430p' frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx

Repository: wecode-ai/Wegent

Length of output: 1510


🏁 Script executed:

# Double-check handleSendMessage uses t() at other places too
sed -n '450,763p' frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx | grep -n "t('chat:" | head -10

Repository: wecode-ai/Wegent

Length of output: 224


🏁 Script executed:

# Verify the full context - where does handleSendMessage end and what's at 764-804
sed -n '800,810p' frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx

Repository: wecode-ai/Wegent

Length of output: 498


🏁 Script executed:

# Confirm the exact line where handleSendMessage dependency array starts and ends
sed -n '760,806p' frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx

Repository: wecode-ai/Wegent

Length of output: 1029


Add t to the handleSendMessage dependencies.

The callback uses t() for translating error messages at lines 459-460, but t is missing from the dependency array, which will cause stale translations after language changes and violates react-hooks/exhaustive-deps.

Proposed fix
       effectiveDeviceId,
       effectiveRequiresWorkspace,
       additionalSkills,
+      t,
       generateParams,
     ]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
title: t('chat:errors.team_not_selected') || '请选择智能体',
description: t('chat:errors.team_not_selected_description') || '请从列表中选择一个智能体后开始对话',
effectiveDeviceId,
effectiveRequiresWorkspace,
additionalSkills,
t,
generateParams,
]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx` around
lines 459 - 460, The handleSendMessage callback uses the i18n translator t() for
error messages but t is not included in its dependency array, causing stale
translations and an exhaustive-deps lint violation; update the dependency array
of handleSendMessage to include t (the i18n translate function) so the callback
is recreated when the language changes, ensuring lines that call
t('chat:errors.team_not_selected') and
t('chat:errors.team_not_selected_description') use current translations.

})
return
}
Comment on lines +455 to +463
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Apply the team guard to model-override sends too.

handleSendMessageWithModel can still send team_id: 0 when selectedTeam is missing, so regeneration/model-override sends can bypass the new protection.

Proposed fix
       const message = overrideMessage.trim()
       if (!message && !shouldHideChatInput) return
 
+      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',
           title: t('chat:upload.wait_for_upload'),
@@
-            team_id: selectedTeam?.id ?? 0,
+            team_id: selectedTeam.id,

Also applies to: 938-942

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx` around
lines 455 - 463, The model-override/regeneration path is missing the same team
check and can send team_id: 0; update the send handlers to guard against missing
selectedTeam before sending by reusing the existing check logic (toast + return)
used earlier, specifically add the selectedTeam?.id guard in
handleSendMessageWithModel and in the other send/regenerate handler that
constructs messages for model-override (the same code path referenced around the
other send function), ensuring any send always early-returns with the
destructive toast when selectedTeam is absent instead of sending team_id: 0.


if (!isAttachmentReadyToSend) {
toast({
variant: 'destructive',
Expand Down
31 changes: 19 additions & 12 deletions frontend/src/features/tasks/contexts/taskContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/i18n/locales/en/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/i18n/locales/zh-CN/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down
Loading