diff --git a/frontend/src/app/(tasks)/chat/ChatPageDesktop.tsx b/frontend/src/app/(tasks)/chat/ChatPageDesktop.tsx index 68e117445..261072c85 100644 --- a/frontend/src/app/(tasks)/chat/ChatPageDesktop.tsx +++ b/frontend/src/app/(tasks)/chat/ChatPageDesktop.tsx @@ -59,16 +59,29 @@ export function ChatPageDesktop() { const { selectedDeviceId, devices } = useDevices() const selectedDevice = devices.find(d => d.device_id === selectedDeviceId) + // For existing tasks, also check the task's device_id + const taskDevice = selectedTaskDetail?.device_id + ? devices.find(d => d.device_id === selectedTaskDetail.device_id) + : null + const isTaskDeviceDeleted = + selectedTaskDetail?.device_id && + !devices.some(d => d.device_id === selectedTaskDetail.device_id) + const isTaskDeviceOffline = taskDevice?.status === 'offline' + // Determine taskType based on device selection // When a device is selected, use 'task' mode (same as /devices/chat) // Otherwise, use 'chat' mode const taskType = selectedDeviceId ? 'task' : 'chat' // Compute disabled reason for device mode - const disabledReason = - selectedDeviceId && (!selectedDevice || selectedDevice.status === 'offline') + // Consider both currently selected device and task's associated device + const disabledReason = isTaskDeviceDeleted + ? t('devices:device_deleted_hint') + : isTaskDeviceOffline ? t('devices:device_offline_cannot_send') - : undefined + : selectedDeviceId && (!selectedDevice || selectedDevice.status === 'offline') + ? t('devices:device_offline_cannot_send') + : undefined // Get current task title for top navigation const currentTaskTitle = selectedTaskDetail?.title diff --git a/frontend/src/app/(tasks)/chat/ChatPageMobile.tsx b/frontend/src/app/(tasks)/chat/ChatPageMobile.tsx index 2d6a6397e..8ce1f1a2d 100644 --- a/frontend/src/app/(tasks)/chat/ChatPageMobile.tsx +++ b/frontend/src/app/(tasks)/chat/ChatPageMobile.tsx @@ -48,16 +48,29 @@ export function ChatPageMobile() { const { selectedDeviceId, devices } = useDevices() const selectedDevice = devices.find(d => d.device_id === selectedDeviceId) + // For existing tasks, also check the task's device_id + const taskDevice = selectedTaskDetail?.device_id + ? devices.find(d => d.device_id === selectedTaskDetail.device_id) + : null + const isTaskDeviceDeleted = + selectedTaskDetail?.device_id && + !devices.some(d => d.device_id === selectedTaskDetail.device_id) + const isTaskDeviceOffline = taskDevice?.status === 'offline' + // Determine taskType based on device selection // When a device is selected, use 'task' mode (same as /devices/chat) // Otherwise, use 'chat' mode const taskType = selectedDeviceId ? 'task' : 'chat' // Compute disabled reason for device mode - const disabledReason = - selectedDeviceId && (!selectedDevice || selectedDevice.status === 'offline') + // Consider both currently selected device and task's associated device + const disabledReason = isTaskDeviceDeleted + ? t('devices:device_deleted_hint') + : isTaskDeviceOffline ? t('devices:device_offline_cannot_send') - : undefined + : selectedDeviceId && (!selectedDevice || selectedDevice.status === 'offline') + ? t('devices:device_offline_cannot_send') + : undefined // Get current task title for top navigation const currentTaskTitle = selectedTaskDetail?.title diff --git a/frontend/src/app/(tasks)/devices/chat/page.tsx b/frontend/src/app/(tasks)/devices/chat/page.tsx index e0556f339..2f844e9fa 100644 --- a/frontend/src/app/(tasks)/devices/chat/page.tsx +++ b/frontend/src/app/(tasks)/devices/chat/page.tsx @@ -4,7 +4,7 @@ 'use client' -import { useEffect, useState, useCallback } from 'react' +import { useEffect, useState, useCallback, useMemo } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import TopNavigation from '@/features/layout/TopNavigation' import { @@ -123,9 +123,22 @@ export default function DeviceChatPage() { // Get selected device info const selectedDevice = devices.find(d => d.device_id === selectedDeviceId) + // For existing tasks, use task's device_id instead of selectedDeviceId for display + const taskDevice = selectedTaskDetail?.device_id + ? devices.find(d => d.device_id === selectedTaskDetail.device_id) + : null + // Task is considered to have messages if it has a non-empty title and was created + const hasMessages = !!(selectedTaskDetail && selectedTaskDetail.id) + // Check if selected device is OpenClaw type const isOpenClaw = selectedDevice ? isOpenClawDevice(selectedDevice) : false + // Check if task was associated with a device that has been deleted + const isTaskDeviceDeleted = useMemo(() => { + if (!selectedTaskDetail?.device_id) return false + return !devices.some(d => d.device_id === selectedTaskDetail.device_id) + }, [selectedTaskDetail?.device_id, devices]) + return (
{/* URL parameter sync */} @@ -162,30 +175,57 @@ export default function DeviceChatPage() { onMembersChanged={handleMembersChanged} isSidebarCollapsed={isCollapsed} > - {/* Device selector in top bar */} -
- - -
+ {/* Device selector in top bar - hide when task's device is deleted */} + {!isTaskDeviceDeleted && ( +
+ + {/* For existing tasks with messages, show read-only device info */} + {hasMessages && taskDevice ? ( +
+ {taskDevice.name} + + + ( + {taskDevice.status === 'online' + ? t('status_online') + : taskDevice.status === 'busy' + ? t('status_busy') + : t('status_offline')} + ) + +
+ ) : ( + + )} +
+ )} {isMobile ? : } @@ -199,9 +239,15 @@ export default function DeviceChatPage() { taskType="task" onRefreshTeams={handleRefreshTeams} disabledReason={ - !selectedDevice || selectedDevice.status === 'offline' - ? t('device_offline_cannot_send') - : undefined + isTaskDeviceDeleted + ? t('device_deleted_hint') + : hasMessages && taskDevice?.status === 'offline' + ? t('device_offline_cannot_send') + : hasMessages && !selectedTaskDetail?.device_id + ? undefined // Task was created with cloud mode, allow sending + : !selectedDevice || selectedDevice.status === 'offline' + ? t('device_offline_cannot_send') + : undefined } hideSelectors={isOpenClaw} /> diff --git a/frontend/src/features/tasks/components/chat/ChatArea.tsx b/frontend/src/features/tasks/components/chat/ChatArea.tsx index e2bef95dc..d7fc0b18f 100644 --- a/frontend/src/features/tasks/components/chat/ChatArea.tsx +++ b/frontend/src/features/tasks/components/chat/ChatArea.tsx @@ -35,6 +35,7 @@ import { useAttachmentUpload } from '../hooks/useAttachmentUpload' import { useSchemeMessageActions } from '@/lib/scheme' import { useSkillSelector } from '../../hooks/useSkillSelector' import { useModelSelection } from '../../hooks/useModelSelection' +import { useDevices } from '@/contexts/DeviceContext' /** * Threshold in pixels for determining when to collapse selectors. @@ -112,6 +113,15 @@ function ChatAreaContent({ // Task context const { selectedTaskDetail, setSelectedTask, accessDenied } = useTaskContext() + // Device context - for detecting deleted device + const { devices } = useDevices() + + // Check if task was associated with a device that has been deleted + const isTaskDeviceDeleted = useMemo(() => { + if (!selectedTaskDetail?.device_id) return false + return !devices.some(d => d.device_id === selectedTaskDetail.device_id) + }, [selectedTaskDetail?.device_id, devices]) + // Use useTaskStateMachine hook for reactive state updates (SINGLE SOURCE OF TRUTH per AGENTS.md) const { state: taskState } = useTaskStateMachine(selectedTaskDetail?.id) @@ -1040,6 +1050,8 @@ function ChatAreaContent({ onGenerateModeChange, // Hide all selectors (for OpenClaw devices) hideSelectors, + // Whether the task's device has been deleted + isTaskDeviceDeleted, } return ( diff --git a/frontend/src/features/tasks/components/input/ChatInputCard.tsx b/frontend/src/features/tasks/components/input/ChatInputCard.tsx index 14239d141..7ee06eb32 100644 --- a/frontend/src/features/tasks/components/input/ChatInputCard.tsx +++ b/frontend/src/features/tasks/components/input/ChatInputCard.tsx @@ -5,7 +5,8 @@ 'use client' import React, { useRef, useState, useCallback } from 'react' -import { Upload, Sparkles } from 'lucide-react' +import Link from 'next/link' +import { Upload, Sparkles, Plus } from 'lucide-react' import ChatInput from './ChatInput' import InputBadgeDisplay from './InputBadgeDisplay' import ExternalApiParamsInput from '../params/ExternalApiParamsInput' @@ -16,6 +17,7 @@ import { QuoteCard } from '../text-selection' import { ConnectionStatusBanner } from './ConnectionStatusBanner' import type { Team, ChatTipItem, TaskType } from '@/types/api' import { useTranslation } from '@/hooks/useTranslation' +import { paths } from '@/config/paths' import type { SkillSelectorPopoverRef } from '../selector/SkillSelectorPopover' export interface ChatInputCardProps extends Omit< @@ -76,6 +78,9 @@ export interface ChatInputCardProps extends Omit< // Reason why input is disabled (e.g., device offline). Shows as placeholder text. disabledReason?: string + // Whether the task's device has been deleted (shows clickable prompt overlay) + isTaskDeviceDeleted?: boolean + // Hide all selectors (for OpenClaw devices) - only show text input + send button hideSelectors?: boolean } @@ -121,6 +126,7 @@ export function ChatInputCard({ inputControlsRef, hasNoTeams = false, disabledReason, + isTaskDeviceDeleted = false, hideSelectors, // ChatInputControls props selectedModel, @@ -260,6 +266,20 @@ export function ChatInputCard({
)} + {/* Device Deleted Overlay - shows clickable prompt when task's device is deleted */} + {isTaskDeviceDeleted && ( +
+

{t('devices:device_deleted_hint')}

+ + + {t('devices:start_new_chat')} + +
+ )} + {/* Unified Badge Display - Knowledge bases and attachments */} { + if (!hasMessages || !taskDeviceId) return false + // If taskDeviceId exists but device not found in devices list, it means the device was deleted + return !devices.some(device => device.device_id === taskDeviceId) + }, [hasMessages, taskDeviceId, devices]) + useEffect(() => { if (hasMessages || isLoading || autoSelectionInitializedRef.current) { return @@ -450,6 +457,40 @@ export function DeviceSelectorTab({ // Read-only mode for existing chats if (hasMessages) { + // When device is deleted, don't show any selector - the prompt will be shown in the input area + if (isTaskDeviceDeleted) { + return null + } + + // If task has device_id but device not found (and not deleted), don't show "Public Mode" + // This handles the case where devices list is still loading or data inconsistency + if (taskDeviceId && !selectedDevice) { + // Show a loading/unknown state instead of misleading "Public Mode" + return ( + + + +
+ + {t('local_device_prefix')}... + +
+
+ +

{t('device_offline_hint')}

+
+
+
+ ) + } + return ( diff --git a/frontend/src/i18n/locales/en/devices.json b/frontend/src/i18n/locales/en/devices.json index 1c89700a8..affbcde1c 100644 --- a/frontend/src/i18n/locales/en/devices.json +++ b/frontend/src/i18n/locales/en/devices.json @@ -37,6 +37,9 @@ "select_device_hint": "Select a device from the top to start sending tasks", "device_offline_hint": "Device is offline, please select another", "device_offline_cannot_send": "Device is offline, cannot send tasks", + "device_deleted": "Device Deleted", + "device_deleted_hint": "The device associated with this chat has been deleted", + "start_new_chat": "Start New Chat", "device_busy_hint": "Device is busy with another task", "select_another_device": "Please select another device or wait for current task to complete", "refresh": "Refresh", diff --git a/frontend/src/i18n/locales/zh-CN/devices.json b/frontend/src/i18n/locales/zh-CN/devices.json index a3576affc..7299dbb80 100644 --- a/frontend/src/i18n/locales/zh-CN/devices.json +++ b/frontend/src/i18n/locales/zh-CN/devices.json @@ -37,6 +37,9 @@ "select_device_hint": "从顶部选择一个在线设备开始发送任务", "device_offline_hint": "设备已离线,请选择其他设备", "device_offline_cannot_send": "设备不在线,无法发送任务", + "device_deleted": "设备已删除", + "device_deleted_hint": "此会话关联的设备已被删除", + "start_new_chat": "开始新对话", "device_busy_hint": "设备正在执行其他任务", "select_another_device": "请选择其他设备或等待当前任务完成", "refresh": "刷新",