-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Avoid extra SSH prompt expiry renders #2582
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5bf11ea
f37bfbd
a811a7c
e06925c
7ead5c7
6ff98af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,13 @@ | ||
| import type { DesktopSshPasswordPromptRequest } from "@t3tools/contracts"; | ||
| import { useEffect, useId, useRef, useState } from "react"; | ||
| import { | ||
| useEffect, | ||
| useId, | ||
| useReducer, | ||
| useRef, | ||
| useState, | ||
| type Dispatch, | ||
| type RefObject, | ||
| } from "react"; | ||
|
|
||
| import { Button } from "../ui/button"; | ||
| import { | ||
|
|
@@ -28,16 +36,41 @@ function getPromptErrorMessage(error: unknown): string { | |
| : message; | ||
| } | ||
|
|
||
| export function SshPasswordPromptDialog() { | ||
| const EXPIRED_PROMPT_MESSAGE = "This SSH password prompt expired. Try connecting again."; | ||
|
|
||
| type PromptDraftState = { | ||
| password: string; | ||
| responseError: string | null; | ||
| now: number; | ||
| }; | ||
|
|
||
| type PromptDraftAction = | ||
| | { type: "prompt.reset"; now: number } | ||
| | { type: "clock.tick"; now: number } | ||
| | { type: "password.set"; password: string } | ||
| | { type: "error.set"; error: string | null }; | ||
|
|
||
| function promptDraftReducer(state: PromptDraftState, action: PromptDraftAction): PromptDraftState { | ||
| switch (action.type) { | ||
| case "prompt.reset": | ||
| return { | ||
| password: "", | ||
| responseError: null, | ||
| now: action.now, | ||
| }; | ||
| case "clock.tick": | ||
| return state.now === action.now ? state : { ...state, now: action.now }; | ||
| case "password.set": | ||
| return state.password === action.password ? state : { ...state, password: action.password }; | ||
| case "error.set": | ||
| return state.responseError === action.error | ||
| ? state | ||
| : { ...state, responseError: action.error }; | ||
| } | ||
| } | ||
|
|
||
| function useSshPasswordPromptQueue() { | ||
| const [queue, setQueue] = useState<readonly DesktopSshPasswordPromptRequest[]>([]); | ||
| const [password, setPassword] = useState(""); | ||
| const [isResponding, setIsResponding] = useState(false); | ||
| const [now, setNow] = useState(() => Date.now()); | ||
| const [responseError, setResponseError] = useState<string | null>(null); | ||
| const currentRequest = queue[0] ?? null; | ||
| const inputRef = useRef<HTMLInputElement | null>(null); | ||
| const isRespondingRef = useRef(false); | ||
| const formId = useId(); | ||
|
|
||
| useEffect(() => { | ||
| const bridge = window.desktopBridge; | ||
|
|
@@ -50,55 +83,70 @@ export function SshPasswordPromptDialog() { | |
| }); | ||
| }, []); | ||
|
|
||
| return [queue, setQueue] as const; | ||
| } | ||
|
|
||
| function useCurrentPromptLifecycle( | ||
| currentRequest: DesktopSshPasswordPromptRequest | null, | ||
| inputRef: RefObject<HTMLInputElement | null>, | ||
| dispatch: Dispatch<PromptDraftAction>, | ||
| ) { | ||
| useEffect(() => { | ||
| setPassword(""); | ||
| setResponseError(null); | ||
| dispatch({ type: "prompt.reset", now: Date.now() }); | ||
| if (!currentRequest) { | ||
| return; | ||
| } | ||
|
|
||
| setNow(Date.now()); | ||
| const frame = window.requestAnimationFrame(() => { | ||
| inputRef.current?.focus(); | ||
| inputRef.current?.select(); | ||
| }); | ||
| return () => { | ||
| window.cancelAnimationFrame(frame); | ||
| }; | ||
| }, [currentRequest]); | ||
| }, [currentRequest, dispatch, inputRef]); | ||
|
|
||
| useEffect(() => { | ||
| if (!currentRequest) { | ||
| return; | ||
| } | ||
|
|
||
| const interval = window.setInterval(() => { | ||
| setNow(Date.now()); | ||
| dispatch({ type: "clock.tick", now: Date.now() }); | ||
| }, 1_000); | ||
| return () => { | ||
| window.clearInterval(interval); | ||
| }; | ||
| }, [currentRequest]); | ||
| }, [currentRequest, dispatch]); | ||
| } | ||
|
|
||
| export function SshPasswordPromptDialog() { | ||
| const [queue, setQueue] = useSshPasswordPromptQueue(); | ||
| const [isResponding, setIsResponding] = useState(false); | ||
| const [draft, dispatchDraft] = useReducer(promptDraftReducer, undefined, () => ({ | ||
| password: "", | ||
| responseError: null, | ||
| now: Date.now(), | ||
| })); | ||
| const currentRequest = queue[0] ?? null; | ||
| const inputRef = useRef<HTMLInputElement | null>(null); | ||
| const isRespondingRef = useRef(false); | ||
| const formId = useId(); | ||
| useCurrentPromptLifecycle(currentRequest, inputRef, dispatchDraft); | ||
|
|
||
| const expiresAtMs = currentRequest ? Date.parse(currentRequest.expiresAt) : Number.NaN; | ||
| const remainingMs = Number.isFinite(expiresAtMs) ? Math.max(0, expiresAtMs - now) : null; | ||
| const remainingMs = Number.isFinite(expiresAtMs) ? Math.max(0, expiresAtMs - draft.now) : null; | ||
| const isExpired = remainingMs !== null && remainingMs <= 0; | ||
| const remainingSeconds = remainingMs === null ? null : Math.ceil(remainingMs / 1_000); | ||
| const remainingLabel = | ||
| remainingSeconds === null ? null : formatRemainingSeconds(remainingSeconds); | ||
|
|
||
| useEffect(() => { | ||
| if (isExpired) { | ||
| setResponseError("This SSH password prompt expired. Try connecting again."); | ||
| } | ||
| }, [isExpired]); | ||
| const visibleResponseError = isExpired ? EXPIRED_PROMPT_MESSAGE : draft.responseError; | ||
|
|
||
| const removeCurrentPrompt = (requestId: string) => { | ||
| setQueue((currentQueue) => | ||
| currentQueue[0]?.requestId === requestId ? currentQueue.slice(1) : currentQueue, | ||
| ); | ||
| setPassword(""); | ||
| setResponseError(null); | ||
| dispatchDraft({ type: "prompt.reset", now: Date.now() }); | ||
| }; | ||
|
|
||
| const respond = async (nextPassword: string | null) => { | ||
|
|
@@ -108,21 +156,21 @@ export function SshPasswordPromptDialog() { | |
|
|
||
| const requestId = currentRequest.requestId; | ||
| if (nextPassword !== null && isExpired) { | ||
| setResponseError("This SSH password prompt expired. Try connecting again."); | ||
| dispatchDraft({ type: "error.set", error: EXPIRED_PROMPT_MESSAGE }); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redundant expired-state dispatch in respond handlerLow Severity The Reviewed by Cursor Bugbot for commit 6ff98af. Configure here. |
||
| return; | ||
| } | ||
|
|
||
| isRespondingRef.current = true; | ||
| setIsResponding(true); | ||
| setResponseError(null); | ||
| dispatchDraft({ type: "error.set", error: null }); | ||
| try { | ||
| await window.desktopBridge?.resolveSshPasswordPrompt(requestId, nextPassword); | ||
| removeCurrentPrompt(requestId); | ||
| } catch (error) { | ||
| if (nextPassword === null) { | ||
| removeCurrentPrompt(requestId); | ||
| } else { | ||
| setResponseError(getPromptErrorMessage(error)); | ||
| dispatchDraft({ type: "error.set", error: getPromptErrorMessage(error) }); | ||
| } | ||
| } finally { | ||
| isRespondingRef.current = false; | ||
|
|
@@ -170,7 +218,7 @@ export function SshPasswordPromptDialog() { | |
| id={formId} | ||
| onSubmit={(event) => { | ||
| event.preventDefault(); | ||
| void respond(password); | ||
| void respond(draft.password); | ||
| }} | ||
| > | ||
| <div className="space-y-2"> | ||
|
|
@@ -194,12 +242,14 @@ export function SshPasswordPromptDialog() { | |
| disabled={isResponding || isExpired} | ||
| name="ssh-password" | ||
| type="password" | ||
| value={password} | ||
| onChange={(event) => setPassword(event.target.value)} | ||
| value={draft.password} | ||
| onChange={(event) => | ||
| dispatchDraft({ type: "password.set", password: event.target.value }) | ||
| } | ||
| /> | ||
| </div> | ||
| {responseError ? ( | ||
| <p className="text-sm text-destructive">{responseError}</p> | ||
| {visibleResponseError ? ( | ||
| <p className="text-sm text-destructive">{visibleResponseError}</p> | ||
| ) : ( | ||
| <p className="text-sm text-muted-foreground"> | ||
| Use SSH keys to avoid repeated password prompts on new SSH sessions. | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate expired-prompt string constant not consolidated
Low Severity
This PR introduces
EXPIRED_PROMPT_MESSAGEas a named constant, butgetPromptErrorMessage(line 35) still contains the identical string literal inline instead of referencing the new constant. The two definitions are in sync today, but if one is updated without the other, users could see different expiry messages depending on whether the prompt expired locally vs. the API returning an expiry error.Additional Locations (1)
apps/web/src/components/desktop/SshPasswordPromptDialog.tsx#L33-L36Reviewed by Cursor Bugbot for commit 6ff98af. Configure here.