adding task start countdown to indicate timeout auto start process (#1621)

This commit is contained in:
Douglas Lai 2026-05-07 20:06:13 +01:00 committed by GitHub
parent 29ecdb6267
commit b2ac129cd8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 118 additions and 27 deletions

View file

@ -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<number | null>(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 (
<div
className={cn(
@ -56,15 +78,27 @@ export const BoxHeaderConfirm = ({
<ChevronLeft />
</Button>
<Button
variant="success"
size="sm"
className="rounded-full"
onClick={onStartTask}
disabled={loading}
>
{t('chat.start-task')}
</Button>
<div className="gap-2 flex items-center">
{remainingSeconds !== null && (
<span
className="text-body-xs font-medium text-ds-text-success-default-default whitespace-nowrap tabular-nums"
aria-label={t('chat.auto-start-in', {
seconds: remainingSeconds,
})}
>
{t('chat.auto-start-in', { seconds: remainingSeconds })}
</span>
)}
<Button
variant="success"
size="sm"
className="rounded-full"
onClick={onStartTask}
disabled={loading}
>
{t('chat.start-task')}
</Button>
</div>
</div>
</div>
);
@ -85,7 +119,7 @@ export interface BoxHeaderSaveProps {
}
export const BoxHeaderSave = ({
subtitle,
subtitle: _subtitle,
onSave,
onEdit,
className,

View file

@ -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' && (

View file

@ -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) {

View file

@ -64,7 +64,8 @@
"splitting-tasks": "تقسيم المهام",
"working-on-tasks-for": "العمل على المهام لمدة <elapsed>{{time}}</elapsed>",
"worked-for": "عُمل لمدة <elapsed>{{time}}</elapsed>",
"start-task": "بدء المهمة",
"start-task": "نفّذ الآن",
"auto-start-in": "بدء تلقائي خلال {{seconds}}ث",
"message-cannot-be-empty": "لا يمكن أن تكون الرسالة فارغة",
"remove-file": "إزالة الملف",
"drop-files-to-attach": "أسقط الملفات للإرفاق",

View file

@ -64,7 +64,8 @@
"splitting-tasks": "Aufgaben teilen",
"working-on-tasks-for": "Arbeitet an Aufgaben seit <elapsed>{{time}}</elapsed>",
"worked-for": "Gearbeitet für <elapsed>{{time}}</elapsed>",
"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",

View file

@ -64,7 +64,8 @@
"splitting-tasks": "Splitting Tasks",
"working-on-tasks-for": "Working on tasks for <elapsed>{{time}}</elapsed>",
"worked-for": "Worked for <elapsed>{{time}}</elapsed>",
"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",

View file

@ -64,7 +64,8 @@
"splitting-tasks": "Dividiendo tareas",
"working-on-tasks-for": "Trabajando en tareas durante <elapsed>{{time}}</elapsed>",
"worked-for": "Trabajó durante <elapsed>{{time}}</elapsed>",
"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",

View file

@ -64,7 +64,8 @@
"splitting-tasks": "Division des tâches",
"working-on-tasks-for": "Travaille sur les tâches depuis <elapsed>{{time}}</elapsed>",
"worked-for": "A travaillé pendant <elapsed>{{time}}</elapsed>",
"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",

View file

@ -64,7 +64,8 @@
"splitting-tasks": "Suddivisione dei compiti",
"working-on-tasks-for": "Lavoro alle attività da <elapsed>{{time}}</elapsed>",
"worked-for": "Ha lavorato per <elapsed>{{time}}</elapsed>",
"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",

View file

@ -64,7 +64,8 @@
"splitting-tasks": "タスク分割",
"working-on-tasks-for": "タスクを実行中 · <elapsed>{{time}}</elapsed>",
"worked-for": "作業時間 <elapsed>{{time}}</elapsed>",
"start-task": "タスク開始",
"start-task": "今すぐ実行",
"auto-start-in": "{{seconds}}秒後に自動開始",
"message-cannot-be-empty": "メッセージは空にできません",
"remove-file": "ファイルを削除",
"drop-files-to-attach": "ファイルをドロップして添付",

View file

@ -64,7 +64,8 @@
"splitting-tasks": "작업 분할",
"working-on-tasks-for": "작업 진행 중 · <elapsed>{{time}}</elapsed>",
"worked-for": "작업 시간 <elapsed>{{time}}</elapsed>",
"start-task": "작업 시작",
"start-task": "지금 실행",
"auto-start-in": "{{seconds}}초 후 자동 시작",
"message-cannot-be-empty": "메시지는 비워둘 수 없습니다",
"remove-file": "파일 제거",
"drop-files-to-attach": "첨부할 파일을 여기에 놓으세요",

View file

@ -64,7 +64,8 @@
"splitting-tasks": "Разделение задач",
"working-on-tasks-for": "Работа над задачами · <elapsed>{{time}}</elapsed>",
"worked-for": "Работал <elapsed>{{time}}</elapsed>",
"start-task": "Начать задачу",
"start-task": "Выполнить сейчас",
"auto-start-in": "Автозапуск через {{seconds}} с",
"message-cannot-be-empty": "Сообщение не может быть пустым",
"remove-file": "Удалить файл",
"drop-files-to-attach": "Перетащите файлы для прикрепления",

View file

@ -64,7 +64,8 @@
"splitting-tasks": "拆分任务",
"working-on-tasks-for": "正在处理任务 · <elapsed>{{time}}</elapsed>",
"worked-for": "已工作 <elapsed>{{time}}</elapsed>",
"start-task": "开始任务",
"start-task": "立即执行",
"auto-start-in": "{{seconds}}秒后自动开始",
"message-cannot-be-empty": "消息不能为空",
"remove-file": "移除文件",
"drop-files-to-attach": "拖放文件以附加",

View file

@ -64,7 +64,8 @@
"splitting-tasks": "拆分任務",
"working-on-tasks-for": "正在處理任務 · <elapsed>{{time}}</elapsed>",
"worked-for": "已工作 <elapsed>{{time}}</elapsed>",
"start-task": "開始任務",
"start-task": "立即執行",
"auto-start-in": "{{seconds}}秒後自動開始",
"message-cannot-be-empty": "訊息不能為空",
"remove-file": "移除文件",
"drop-files-to-attach": "拖放文件以附加",

View file

@ -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<void>;
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<string, ReturnType<typeof setTimeout>> = {};
const AUTO_CONFIRM_TIMEOUT_MS = 30000;
// Track active SSE connections for proper cleanup
const activeSSEControllers: Record<string, AbortController> = {};
@ -849,6 +852,7 @@ const chatStore = (initial?: Partial<ChatStore>) =>
snapshotsTemp: [],
isTakeControl: false,
planDirty: false,
autoConfirmDeadline: null,
streamingDecomposeText: '',
executionId: undefined,
},
@ -877,6 +881,7 @@ const chatStore = (initial?: Partial<ChatStore>) =>
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<ChatStore>) =>
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<ChatStore>) =>
setStreamingDecomposeText,
clearStreamingDecomposeText,
setPlanDirty,
setAutoConfirmDeadline,
} = getCurrentChatStore();
currentTaskId = getCurrentTaskId();
@ -1694,18 +1701,27 @@ const chatStore = (initial?: Partial<ChatStore>) =>
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<ChatStore>) =>
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<ChatStore>) =>
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<ChatStore>) =>
setTaskInfo,
setTaskRunning,
setPlanDirty,
setAutoConfirmDeadline,
} = get();
if (!taskId) return;
@ -3446,6 +3466,7 @@ const chatStore = (initial?: Partial<ChatStore>) =>
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<ChatStore>) =>
},
}));
},
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<ChatStore>) =>
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<ChatStore>) =>
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();