Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .centy/issues/1de638db-d80c-412d-b51a-de860ef907c6.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
27 changes: 27 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down
38 changes: 20 additions & 18 deletions src/app/open/opening-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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<OpeningViewProps, "phase" | "params">) {
return (
<div className="anim-fade-up d-300">
<div className="confirm-section">
Expand All @@ -43,7 +38,23 @@ function ConfirmActions({ onSuccess, onInstall, onRetry }: ConfirmActionsProps)
);
}

function SuccessFooter({ autoClose, countdown, onToggle }: { autoClose: boolean; countdown: number; onToggle: (v: boolean) => void }) {
return (
<div className="anim-fade-up success-footer">
<p className="close-tab-text">
{autoClose ? `Closing in ${countdown}\u2026` : "You can close this tab."}
</p>
<label className="auto-close-toggle">
<input type="checkbox" checked={autoClose} onChange={(e) => onToggle(e.target.checked)} className="auto-close-checkbox" />
Auto-close tab
</label>
</div>
);
}

export function OpeningView({ phase, params, onSuccess, onInstall, onRetry }: OpeningViewProps) {
const { autoClose, countdown, handleToggle } = useAutoClose(phase === "success");

return (
<div className="phase-content">
<div className="status-row anim-fade-up">
Expand All @@ -67,18 +78,9 @@ export function OpeningView({ phase, params, onSuccess, onInstall, onRetry }: Op
</>
)}
</div>

<IssueCard params={params} />

{phase === "opening" && (
<ConfirmActions onSuccess={onSuccess} onInstall={onInstall} onRetry={onRetry} />
)}

{phase === "success" && (
<div className="anim-fade-up">
<p className="close-tab-text">You can close this tab.</p>
</div>
)}
{phase === "opening" && <ConfirmActions onSuccess={onSuccess} onInstall={onInstall} onRetry={onRetry} />}
{phase === "success" && <SuccessFooter autoClose={autoClose} countdown={countdown} onToggle={handleToggle} />}
</div>
);
}
16 changes: 16 additions & 0 deletions src/app/open/use-auto-close.spec.ts
Original file line number Diff line number Diff line change
@@ -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.
56 changes: 56 additions & 0 deletions src/app/open/use-auto-close.ts
Original file line number Diff line number Diff line change
@@ -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<ReturnType<typeof setInterval> | 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 };
}
Loading