From 4d5969d59e4583083bfefa2fbf0c6364f0a2195e Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Fri, 17 Apr 2026 18:12:27 +0700 Subject: [PATCH] refactor(copilot): simplify cancel-auto-approve route, derive countdown from timestamp - Route always returns cancelled=True, reason branch was dead code since cancel_auto_approve() unconditionally returns True - Use crypto.randomUUID() for new step IDs to avoid same-millisecond collisions on rapid insert clicks - Re-derive secondsLeft from created_at+auto_approve_seconds on each tick (and on visibilitychange) instead of naive decrement; browsers throttle setInterval in background tabs, causing the displayed countdown to drift far behind wall-clock time --- .../backend/api/features/chat/routes.py | 7 +--- .../tools/DecomposeGoal/DecomposeGoal.tsx | 39 +++++++++++++++---- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/autogpt_platform/backend/backend/api/features/chat/routes.py b/autogpt_platform/backend/backend/api/features/chat/routes.py index 3baa4b6b5c..4daf08b185 100644 --- a/autogpt_platform/backend/backend/api/features/chat/routes.py +++ b/autogpt_platform/backend/backend/api/features/chat/routes.py @@ -861,11 +861,8 @@ async def cancel_auto_approve_task( from backend.copilot.tools.decompose_goal import cancel_auto_approve - cancelled = await cancel_auto_approve(session_id) - return CancelSessionResponse( - cancelled=cancelled, - reason=None if cancelled else "no_pending_auto_approve", - ) + await cancel_auto_approve(session_id) + return CancelSessionResponse(cancelled=True) @router.post( diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/DecomposeGoal.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/DecomposeGoal.tsx index fdf3328ca1..8175681f44 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/DecomposeGoal.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/DecomposeGoal.tsx @@ -170,7 +170,7 @@ export function DecomposeGoalTool({ setEditableSteps((prev) => { const next = [...prev]; next.splice(afterIndex + 1, 0, { - step_id: `step_new_${Date.now()}`, + step_id: `step_new_${crypto.randomUUID()}`, description: "", action: "add_block", status: "pending", @@ -186,14 +186,39 @@ export function DecomposeGoalTool({ } }, [showActions, isEditing]); - // Tick down only while the timer is active. + // Re-derive remaining seconds from ``created_at`` on every tick (and on + // tab visibility change) instead of decrementing a local counter. + // ``setInterval`` is aggressively throttled in backgrounded tabs, so a + // naive ``s - 1`` drifts behind wall-clock time — the user could reopen + // the tab well past the deadline and still see e.g. "Starting in 45". + // Re-deriving keeps the UI in sync with the server-side timer. + const createdAt = + output && isDecompositionOutput(output) ? output.created_at : undefined; useEffect(() => { if (!showActions || !timerActive) return; - const interval = setInterval(() => { - setSecondsLeft((s) => Math.max(0, s - 1)); - }, 1000); - return () => clearInterval(interval); - }, [showActions, timerActive, part.toolCallId]); + const deadlineMs = createdAt + ? new Date(createdAt).getTime() + countdownSeconds * 1000 + : null; + function recompute() { + if (deadlineMs !== null && !Number.isNaN(deadlineMs)) { + const remaining = Math.max( + 0, + Math.round((deadlineMs - Date.now()) / 1000), + ); + setSecondsLeft(Math.min(countdownSeconds, remaining)); + } else { + // Legacy session with no ``created_at`` — fall back to naive decrement. + setSecondsLeft((s) => Math.max(0, s - 1)); + } + } + recompute(); + const interval = setInterval(recompute, 1000); + document.addEventListener("visibilitychange", recompute); + return () => { + clearInterval(interval); + document.removeEventListener("visibilitychange", recompute); + }; + }, [showActions, timerActive, part.toolCallId, createdAt, countdownSeconds]); // Auto-approve when countdown reaches 0. The client fires at 60s; the // server fires 5s later as a fallback for the "user closed the tab" case.