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 */}
-
-
- handleDeviceSelect(e.target.value)}
- className="bg-surface border border-border rounded-md px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20"
- >
-
- {t('select_device')}
-
- {devices.map(device => (
-
- {device.name} (
- {device.status === 'online'
- ? t('status_online')
- : device.status === 'busy'
- ? t('status_busy')
- : t('status_offline')}
- )
-
- ))}
-
-
+ {/* 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')}
+ )
+
+
+ ) : (
+
handleDeviceSelect(e.target.value)}
+ className="bg-surface border border-border rounded-md px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20"
+ >
+
+ {t('select_device')}
+
+ {devices.map(device => (
+
+ {device.name} (
+ {device.status === 'online'
+ ? t('status_online')
+ : device.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 && (
+