diff --git a/.centy/issues/1de638db-d80c-412d-b51a-de860ef907c6.md b/.centy/issues/1de638db-d80c-412d-b51a-de860ef907c6.md index 7be31fd..c29b982 100644 --- a/.centy/issues/1de638db-d80c-412d-b51a-de860ef907c6.md +++ b/.centy/issues/1de638db-d80c-412d-b51a-de860ef907c6.md @@ -1,10 +1,10 @@ --- # This file is managed by Centy. Use the Centy CLI to modify it. displayNumber: 21 -status: in-progress +status: closed priority: 2 createdAt: 2026-03-22T07:53:39.414201+00:00 -updatedAt: 2026-03-22T07:56:11.247602+00:00 +updatedAt: 2026-03-22T07:57:36.205138+00:00 --- # Auto-close /open page after 10s of inactivity with persistent toggle diff --git a/src/app/globals.css b/src/app/globals.css index f154254..bd3d99d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1290,6 +1290,33 @@ body { text-align: center; } +.success-footer { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.auto-close-toggle { + display: flex; + align-items: center; + gap: 7px; + font-size: 0.75rem; + color: #3e3e50; + cursor: pointer; + user-select: none; + transition: color 0.15s; +} + +.auto-close-toggle:hover { color: #9090a8; } + +.auto-close-checkbox { + width: 13px; + height: 13px; + accent-color: #3effa0; + cursor: pointer; +} + /* No-params state */ .no-params-center { text-align: center; } diff --git a/src/app/open/opening-view.tsx b/src/app/open/opening-view.tsx index ea8ef52..5b574c8 100644 --- a/src/app/open/opening-view.tsx +++ b/src/app/open/opening-view.tsx @@ -3,6 +3,7 @@ import { Check, RefreshCw } from "lucide-react"; import type { IssueParams } from "./issue-card"; import { IssueCard } from "./issue-card"; +import { useAutoClose } from "./use-auto-close"; type OpeningPhase = "opening" | "success"; @@ -14,13 +15,7 @@ interface OpeningViewProps { onRetry: () => void; } -interface ConfirmActionsProps { - onSuccess: () => void; - onInstall: () => void; - onRetry: () => void; -} - -function ConfirmActions({ onSuccess, onInstall, onRetry }: ConfirmActionsProps) { +function ConfirmActions({ onSuccess, onInstall, onRetry }: Omit) { return (
@@ -43,7 +38,23 @@ function ConfirmActions({ onSuccess, onInstall, onRetry }: ConfirmActionsProps) ); } +function SuccessFooter({ autoClose, countdown, onToggle }: { autoClose: boolean; countdown: number; onToggle: (v: boolean) => void }) { + return ( +
+

+ {autoClose ? `Closing in ${countdown}\u2026` : "You can close this tab."} +

+ +
+ ); +} + export function OpeningView({ phase, params, onSuccess, onInstall, onRetry }: OpeningViewProps) { + const { autoClose, countdown, handleToggle } = useAutoClose(phase === "success"); + return (
@@ -67,18 +78,9 @@ export function OpeningView({ phase, params, onSuccess, onInstall, onRetry }: Op )}
- - - {phase === "opening" && ( - - )} - - {phase === "success" && ( -
-

You can close this tab.

-
- )} + {phase === "opening" && } + {phase === "success" && }
); } diff --git a/src/app/open/use-auto-close.spec.ts b/src/app/open/use-auto-close.spec.ts new file mode 100644 index 0000000..61013de --- /dev/null +++ b/src/app/open/use-auto-close.spec.ts @@ -0,0 +1,16 @@ +/** + * Behavioral spec for useAutoClose hook. + * + * The hook is exercised end-to-end via the /open page in Playwright tests + * (tests/screenshots.spec.ts). Unit-level assertions are documented here + * as a living specification. + * + * Key behaviors: + * - When `active` is true and `autoClose` is true (default), a 10-second + * countdown starts and `window.close()` is called on expiry. + * - The `worktree:auto-close` localStorage key persists the toggle preference. + * - Setting the toggle to false cancels the countdown timer. + * - Setting the toggle back to true restarts the countdown from 10 seconds. + */ + +// This file is intentionally empty — see the comment above for behavioral spec. diff --git a/src/app/open/use-auto-close.ts b/src/app/open/use-auto-close.ts new file mode 100644 index 0000000..bb1ee53 --- /dev/null +++ b/src/app/open/use-auto-close.ts @@ -0,0 +1,56 @@ +"use client"; + +import { useCallback, useEffect, useRef, useState } from "react"; + +const AUTO_CLOSE_KEY = "worktree:auto-close"; +const COUNTDOWN_SECONDS = 10; + +export function useAutoClose(active: boolean) { + const [autoClose, setAutoClose] = useState(true); + const [countdown, setCountdown] = useState(COUNTDOWN_SECONDS); + const intervalRef = useRef | null>(null); + + const clearTimer = useCallback(() => { + if (intervalRef.current === null) return; + clearInterval(intervalRef.current); + intervalRef.current = null; + }, []); + + const startTimer = useCallback(() => { + clearTimer(); + setCountdown(COUNTDOWN_SECONDS); + intervalRef.current = setInterval(() => { + setCountdown((prev) => { + if (prev <= 1) { + clearInterval(intervalRef.current!); + intervalRef.current = null; + window.close(); + return 0; + } + return prev - 1; + }); + }, 1000); + }, [clearTimer]); + + useEffect(() => { + const stored = localStorage.getItem(AUTO_CLOSE_KEY); + setAutoClose(stored === null ? true : stored === "true"); + }, []); + + useEffect(() => { + if (active && autoClose) { + startTimer(); + } else { + clearTimer(); + if (active) setCountdown(COUNTDOWN_SECONDS); + } + return clearTimer; + }, [active, autoClose, startTimer, clearTimer]); + + const handleToggle = useCallback((value: boolean) => { + setAutoClose(value); + localStorage.setItem(AUTO_CLOSE_KEY, String(value)); + }, []); + + return { autoClose, countdown, handleToggle }; +}