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
This commit is contained in:
Zamil Majdy
2026-04-17 18:12:27 +07:00
parent 5542f780d2
commit 4d5969d59e
2 changed files with 34 additions and 12 deletions

View File

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

View File

@@ -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.