From b2ac129cd8ee36dd0b95dcc0e21981af02fffca2 Mon Sep 17 00:00:00 2001 From: Douglas Lai <115660088+Douglasymlai@users.noreply.github.com> Date: Thu, 7 May 2026 20:06:13 +0100 Subject: [PATCH] adding task start countdown to indicate timeout auto start process (#1621) --- .../ChatBox/BottomBox/BoxHeader.tsx | 56 +++++++++++++++---- src/components/ChatBox/BottomBox/index.tsx | 3 + src/components/ChatBox/index.tsx | 3 + src/i18n/locales/ar/chat.json | 3 +- src/i18n/locales/de/chat.json | 3 +- src/i18n/locales/en-us/chat.json | 3 +- src/i18n/locales/es/chat.json | 3 +- src/i18n/locales/fr/chat.json | 3 +- src/i18n/locales/it/chat.json | 3 +- src/i18n/locales/ja/chat.json | 3 +- src/i18n/locales/ko/chat.json | 3 +- src/i18n/locales/ru/chat.json | 3 +- src/i18n/locales/zh-Hans/chat.json | 3 +- src/i18n/locales/zh-Hant/chat.json | 3 +- src/store/chatStore.ts | 50 +++++++++++++++-- 15 files changed, 118 insertions(+), 27 deletions(-) diff --git a/src/components/ChatBox/BottomBox/BoxHeader.tsx b/src/components/ChatBox/BottomBox/BoxHeader.tsx index ebffec5a..2ac615ba 100644 --- a/src/components/ChatBox/BottomBox/BoxHeader.tsx +++ b/src/components/ChatBox/BottomBox/BoxHeader.tsx @@ -15,6 +15,7 @@ import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import { ChevronLeft } from 'lucide-react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; /** @@ -26,16 +27,37 @@ export interface BoxHeaderConfirmProps { onEdit?: () => void; className?: string; loading?: boolean; + autoStartDeadline?: number | null; } export const BoxHeaderConfirm = ({ - subtitle, + subtitle: _subtitle, onStartTask, onEdit, className, loading = false, + autoStartDeadline = null, }: BoxHeaderConfirmProps) => { const { t } = useTranslation(); + const [remainingSeconds, setRemainingSeconds] = useState(null); + + useEffect(() => { + if (!autoStartDeadline) { + setRemainingSeconds(null); + return; + } + + const updateRemainingSeconds = () => { + setRemainingSeconds( + Math.max(0, Math.ceil((autoStartDeadline - Date.now()) / 1000)) + ); + }; + + updateRemainingSeconds(); + const intervalId = window.setInterval(updateRemainingSeconds, 250); + return () => window.clearInterval(intervalId); + }, [autoStartDeadline]); + return (
- +
+ {remainingSeconds !== null && ( + + {t('chat.auto-start-in', { seconds: remainingSeconds })} + + )} + +
); @@ -85,7 +119,7 @@ export interface BoxHeaderSaveProps { } export const BoxHeaderSave = ({ - subtitle, + subtitle: _subtitle, onSave, onEdit, className, diff --git a/src/components/ChatBox/BottomBox/index.tsx b/src/components/ChatBox/BottomBox/index.tsx index 3fc37376..994fed09 100644 --- a/src/components/ChatBox/BottomBox/index.tsx +++ b/src/components/ChatBox/BottomBox/index.tsx @@ -37,6 +37,7 @@ interface BottomBoxProps { // Subtask-related props (confirm/save state) subtitle?: string; + autoStartDeadline?: number | null; // Action buttons onStartTask?: () => void; @@ -67,6 +68,7 @@ export default function BottomBox({ queuedMessages = [], onRemoveQueuedMessage, subtitle, + autoStartDeadline, onStartTask, onSavePlan, onEdit, @@ -105,6 +107,7 @@ export default function BottomBox({ onStartTask={onStartTask} onEdit={onEdit} loading={loading} + autoStartDeadline={autoStartDeadline} /> )} {state === 'save' && ( diff --git a/src/components/ChatBox/index.tsx b/src/components/ChatBox/index.tsx index 70f7ae05..2bed6ffa 100644 --- a/src/components/ChatBox/index.tsx +++ b/src/components/ChatBox/index.tsx @@ -1008,6 +1008,9 @@ export default function ChatBox(): JSX.Element { })() : chatStore.tasks[chatStore.activeTaskId]?.summaryTask } + autoStartDeadline={ + chatStore.tasks[chatStore.activeTaskId]?.autoConfirmDeadline + } onStartTask={() => handleConfirmTask()} onSavePlan={async () => { if (chatStore.activeTaskId) { diff --git a/src/i18n/locales/ar/chat.json b/src/i18n/locales/ar/chat.json index 704bdde7..04608638 100644 --- a/src/i18n/locales/ar/chat.json +++ b/src/i18n/locales/ar/chat.json @@ -64,7 +64,8 @@ "splitting-tasks": "تقسيم المهام", "working-on-tasks-for": "العمل على المهام لمدة {{time}}", "worked-for": "عُمل لمدة {{time}}", - "start-task": "بدء المهمة", + "start-task": "نفّذ الآن", + "auto-start-in": "بدء تلقائي خلال {{seconds}}ث", "message-cannot-be-empty": "لا يمكن أن تكون الرسالة فارغة", "remove-file": "إزالة الملف", "drop-files-to-attach": "أسقط الملفات للإرفاق", diff --git a/src/i18n/locales/de/chat.json b/src/i18n/locales/de/chat.json index de6bf28a..103194c2 100644 --- a/src/i18n/locales/de/chat.json +++ b/src/i18n/locales/de/chat.json @@ -64,7 +64,8 @@ "splitting-tasks": "Aufgaben teilen", "working-on-tasks-for": "Arbeitet an Aufgaben seit {{time}}", "worked-for": "Gearbeitet für {{time}}", - "start-task": "Aufgabe starten", + "start-task": "Jetzt ausführen", + "auto-start-in": "Auto-Start in {{seconds}}s", "message-cannot-be-empty": "Nachricht darf nicht leer sein", "remove-file": "Datei entfernen", "drop-files-to-attach": "Dateien zum Anhängen ablegen", diff --git a/src/i18n/locales/en-us/chat.json b/src/i18n/locales/en-us/chat.json index 54acfddf..9cad529f 100644 --- a/src/i18n/locales/en-us/chat.json +++ b/src/i18n/locales/en-us/chat.json @@ -64,7 +64,8 @@ "splitting-tasks": "Splitting Tasks", "working-on-tasks-for": "Working on tasks for {{time}}", "worked-for": "Worked for {{time}}", - "start-task": "Start Task", + "start-task": "Execute Now", + "auto-start-in": "Auto-start in {{seconds}}s", "message-cannot-be-empty": "Message cannot be empty", "remove-file": "Remove file", "drop-files-to-attach": "Drop files to attach", diff --git a/src/i18n/locales/es/chat.json b/src/i18n/locales/es/chat.json index 7b6fcfbe..50574c8c 100644 --- a/src/i18n/locales/es/chat.json +++ b/src/i18n/locales/es/chat.json @@ -64,7 +64,8 @@ "splitting-tasks": "Dividiendo tareas", "working-on-tasks-for": "Trabajando en tareas durante {{time}}", "worked-for": "Trabajó durante {{time}}", - "start-task": "Iniciar tarea", + "start-task": "Ejecutar ahora", + "auto-start-in": "Inicio automático en {{seconds}}s", "message-cannot-be-empty": "El mensaje no puede estar vacío", "remove-file": "Eliminar archivo", "drop-files-to-attach": "Arrastra archivos para adjuntar", diff --git a/src/i18n/locales/fr/chat.json b/src/i18n/locales/fr/chat.json index 48f15338..587eeba3 100644 --- a/src/i18n/locales/fr/chat.json +++ b/src/i18n/locales/fr/chat.json @@ -64,7 +64,8 @@ "splitting-tasks": "Division des tâches", "working-on-tasks-for": "Travaille sur les tâches depuis {{time}}", "worked-for": "A travaillé pendant {{time}}", - "start-task": "Démarrer la tâche", + "start-task": "Exécuter maintenant", + "auto-start-in": "Démarrage auto dans {{seconds}}s", "message-cannot-be-empty": "Le message ne peut pas être vide", "remove-file": "Supprimer le fichier", "drop-files-to-attach": "Déposez les fichiers à joindre", diff --git a/src/i18n/locales/it/chat.json b/src/i18n/locales/it/chat.json index 1e531036..599204fb 100644 --- a/src/i18n/locales/it/chat.json +++ b/src/i18n/locales/it/chat.json @@ -64,7 +64,8 @@ "splitting-tasks": "Suddivisione dei compiti", "working-on-tasks-for": "Lavoro alle attività da {{time}}", "worked-for": "Ha lavorato per {{time}}", - "start-task": "Avvia compito", + "start-task": "Esegui ora", + "auto-start-in": "Avvio automatico tra {{seconds}}s", "message-cannot-be-empty": "Il messaggio non può essere vuoto", "remove-file": "Rimuovi file", "drop-files-to-attach": "Trascina i file da allegare", diff --git a/src/i18n/locales/ja/chat.json b/src/i18n/locales/ja/chat.json index a6c4178b..91395d4c 100644 --- a/src/i18n/locales/ja/chat.json +++ b/src/i18n/locales/ja/chat.json @@ -64,7 +64,8 @@ "splitting-tasks": "タスク分割", "working-on-tasks-for": "タスクを実行中 · {{time}}", "worked-for": "作業時間 {{time}}", - "start-task": "タスク開始", + "start-task": "今すぐ実行", + "auto-start-in": "{{seconds}}秒後に自動開始", "message-cannot-be-empty": "メッセージは空にできません", "remove-file": "ファイルを削除", "drop-files-to-attach": "ファイルをドロップして添付", diff --git a/src/i18n/locales/ko/chat.json b/src/i18n/locales/ko/chat.json index 896b0ee8..2db2e2f9 100644 --- a/src/i18n/locales/ko/chat.json +++ b/src/i18n/locales/ko/chat.json @@ -64,7 +64,8 @@ "splitting-tasks": "작업 분할", "working-on-tasks-for": "작업 진행 중 · {{time}}", "worked-for": "작업 시간 {{time}}", - "start-task": "작업 시작", + "start-task": "지금 실행", + "auto-start-in": "{{seconds}}초 후 자동 시작", "message-cannot-be-empty": "메시지는 비워둘 수 없습니다", "remove-file": "파일 제거", "drop-files-to-attach": "첨부할 파일을 여기에 놓으세요", diff --git a/src/i18n/locales/ru/chat.json b/src/i18n/locales/ru/chat.json index a0731f1c..b5a1b585 100644 --- a/src/i18n/locales/ru/chat.json +++ b/src/i18n/locales/ru/chat.json @@ -64,7 +64,8 @@ "splitting-tasks": "Разделение задач", "working-on-tasks-for": "Работа над задачами · {{time}}", "worked-for": "Работал {{time}}", - "start-task": "Начать задачу", + "start-task": "Выполнить сейчас", + "auto-start-in": "Автозапуск через {{seconds}} с", "message-cannot-be-empty": "Сообщение не может быть пустым", "remove-file": "Удалить файл", "drop-files-to-attach": "Перетащите файлы для прикрепления", diff --git a/src/i18n/locales/zh-Hans/chat.json b/src/i18n/locales/zh-Hans/chat.json index 52ba1899..6a08df2c 100644 --- a/src/i18n/locales/zh-Hans/chat.json +++ b/src/i18n/locales/zh-Hans/chat.json @@ -64,7 +64,8 @@ "splitting-tasks": "拆分任务", "working-on-tasks-for": "正在处理任务 · {{time}}", "worked-for": "已工作 {{time}}", - "start-task": "开始任务", + "start-task": "立即执行", + "auto-start-in": "{{seconds}}秒后自动开始", "message-cannot-be-empty": "消息不能为空", "remove-file": "移除文件", "drop-files-to-attach": "拖放文件以附加", diff --git a/src/i18n/locales/zh-Hant/chat.json b/src/i18n/locales/zh-Hant/chat.json index f1245979..78475f5d 100644 --- a/src/i18n/locales/zh-Hant/chat.json +++ b/src/i18n/locales/zh-Hant/chat.json @@ -64,7 +64,8 @@ "splitting-tasks": "拆分任務", "working-on-tasks-for": "正在處理任務 · {{time}}", "worked-for": "已工作 {{time}}", - "start-task": "開始任務", + "start-task": "立即執行", + "auto-start-in": "{{seconds}}秒後自動開始", "message-cannot-be-empty": "訊息不能為空", "remove-file": "移除文件", "drop-files-to-attach": "拖放文件以附加", diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index 8a2ec1de..70d0ed2d 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -183,6 +183,7 @@ interface Task { snapshotsTemp: any[]; isTakeControl: boolean; planDirty: boolean; + autoConfirmDeadline: number | null; isContextExceeded?: boolean; // Streaming decompose text - stored separately to avoid frequent re-renders streamingDecomposeText: string; @@ -479,6 +480,7 @@ export interface ChatStore { setIsTakeControl: (taskId: string, isTakeControl: boolean) => void; setSnapshotsTemp: (taskId: string, snapshot: any) => void; setPlanDirty: (taskId: string, dirty: boolean) => void; + setAutoConfirmDeadline: (taskId: string, deadline: number | null) => void; savePlan: (taskId: string) => Promise; clearTasks: () => void; setIsContextExceeded: (taskId: string, isContextExceeded: boolean) => void; @@ -499,6 +501,7 @@ export type VanillaChatStore = { // Track auto-confirm timers per task to avoid reusing stale timers across rounds const autoConfirmTimers: Record> = {}; +const AUTO_CONFIRM_TIMEOUT_MS = 30000; // Track active SSE connections for proper cleanup const activeSSEControllers: Record = {}; @@ -849,6 +852,7 @@ const chatStore = (initial?: Partial) => snapshotsTemp: [], isTakeControl: false, planDirty: false, + autoConfirmDeadline: null, streamingDecomposeText: '', executionId: undefined, }, @@ -877,6 +881,7 @@ const chatStore = (initial?: Partial) => clearTimeout(autoConfirmTimers[taskId]); delete autoConfirmTimers[taskId]; } + get().setAutoConfirmDeadline(taskId, null); } catch (error) { console.warn('Error clearing auto-confirm timer in removeTask:', error); } @@ -948,6 +953,7 @@ const chatStore = (initial?: Partial) => clearTimeout(autoConfirmTimers[taskId]); delete autoConfirmTimers[taskId]; } + get().setAutoConfirmDeadline(taskId, null); } catch (error) { console.warn('Error clearing auto-confirm timer in stopTask:', error); } @@ -1599,6 +1605,7 @@ const chatStore = (initial?: Partial) => setStreamingDecomposeText, clearStreamingDecomposeText, setPlanDirty, + setAutoConfirmDeadline, } = getCurrentChatStore(); currentTaskId = getCurrentTaskId(); @@ -1694,18 +1701,27 @@ const chatStore = (initial?: Partial) => clearTimeout(autoConfirmTimers[currentTaskId]); delete autoConfirmTimers[currentTaskId]; } + setAutoConfirmDeadline(currentTaskId, null); } catch (error) { console.warn('Error clearing auto-confirm timer:', error); } // 30 seconds auto confirm try { + setAutoConfirmDeadline( + currentTaskId, + Date.now() + AUTO_CONFIRM_TIMEOUT_MS + ); autoConfirmTimers[currentTaskId] = setTimeout(() => { try { const currentStore = getCurrentChatStore(); const currentId = getCurrentTaskId(); - const { tasks, handleConfirmTask, setPlanDirty } = - currentStore; + const { + tasks, + handleConfirmTask, + setPlanDirty, + setAutoConfirmDeadline, + } = currentStore; const message = tasks[currentId].messages.findLast( (item) => item.step === AgentStep.TO_SUB_TASKS ); @@ -1721,6 +1737,7 @@ const chatStore = (initial?: Partial) => handleConfirmTask(project_id, currentId, type); } setPlanDirty(currentId, false); + setAutoConfirmDeadline(currentId, null); delete autoConfirmTimers[currentId]; } catch (error) { console.error( @@ -1728,11 +1745,13 @@ const chatStore = (initial?: Partial) => error ); // Clean up the timer reference even if there's an error + setAutoConfirmDeadline(currentTaskId, null); delete autoConfirmTimers[currentTaskId]; } - }, 30000); + }, AUTO_CONFIRM_TIMEOUT_MS); } catch (error) { console.error('Error setting auto-confirm timer:', error); + setAutoConfirmDeadline(currentTaskId, null); } const newNoticeMessage: Message = { @@ -3437,6 +3456,7 @@ const chatStore = (initial?: Partial) => setTaskInfo, setTaskRunning, setPlanDirty, + setAutoConfirmDeadline, } = get(); if (!taskId) return; @@ -3446,6 +3466,7 @@ const chatStore = (initial?: Partial) => clearTimeout(autoConfirmTimers[taskId]); delete autoConfirmTimers[taskId]; } + setAutoConfirmDeadline(taskId, null); } catch (error) { console.warn( 'Error clearing auto-confirm timer in handleConfirmTask:', @@ -3841,8 +3862,23 @@ const chatStore = (initial?: Partial) => }, })); }, + setAutoConfirmDeadline(taskId: string, deadline: number | null) { + set((state) => { + if (!state.tasks[taskId]) return state; + return { + ...state, + tasks: { + ...state.tasks, + [taskId]: { + ...state.tasks[taskId], + autoConfirmDeadline: deadline, + }, + }, + }; + }); + }, async savePlan(taskId: string) { - const { tasks, setPlanDirty } = get(); + const { tasks, setPlanDirty, setAutoConfirmDeadline } = get(); const task = tasks[taskId]; if (!task) return; try { @@ -3872,10 +3908,12 @@ const chatStore = (initial?: Partial) => clearTimeout(autoConfirmTimers[taskId]); delete autoConfirmTimers[taskId]; } + setAutoConfirmDeadline(taskId, null); } catch (error) { console.warn('Error clearing auto-confirm timer in savePlan:', error); } + setAutoConfirmDeadline(taskId, Date.now() + AUTO_CONFIRM_TIMEOUT_MS); autoConfirmTimers[taskId] = setTimeout(() => { try { const latestState = get(); @@ -3894,12 +3932,14 @@ const chatStore = (initial?: Partial) => latestState.handleConfirmTask(projectId, taskId); } latestState.setPlanDirty(taskId, false); + latestState.setAutoConfirmDeadline(taskId, null); delete autoConfirmTimers[taskId]; } catch (error) { console.error('Error in savePlan auto-confirm handler:', error); + get().setAutoConfirmDeadline(taskId, null); delete autoConfirmTimers[taskId]; } - }, 30000); + }, AUTO_CONFIRM_TIMEOUT_MS); }, clearTasks: () => { const { create } = get();