Skip to content
Open
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
116 changes: 83 additions & 33 deletions apps/web/src/components/desktop/SshPasswordPromptDialog.tsx
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 {
Expand Down Expand Up @@ -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.";
Copy link
Copy Markdown
Contributor Author

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_MESSAGE as a named constant, but getPromptErrorMessage (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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6ff98af. Configure here.


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;
Expand All @@ -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) => {
Expand All @@ -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 });
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant expired-state dispatch in respond handler

Low Severity

The dispatchDraft({ type: "error.set", error: EXPIRED_PROMPT_MESSAGE }) call on the expired-password path in respond is now a no-op. Because visibleResponseError is derived as isExpired ? EXPIRED_PROMPT_MESSAGE : draft.responseError, setting draft.responseError when isExpired is already true has no visible effect. This is a leftover from the pre-refactor code where the error state directly drove the rendered message, and it now misrepresents how expiry display works.

Fix in Cursor Fix in Web

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;
Expand Down Expand Up @@ -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">
Expand All @@ -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.
Expand Down
Loading