refactor(credential-management): improve regex validation and prompt handling (#67)

- Updated regex validation logic to allow empty inputs for username and password prompts, enhancing user experience.
- Changed labels to indicate optional fields for username and password regex inputs.
- Refactored credential handling to improve clarity and maintainability in the CredentialManagementTab component.
- Introduced new utility functions for better prompt detection and fallback credential handling in the credential autofill logic.
This commit is contained in:
Kang committed 2026-06-05 01:09:30 +08:00
1 parent 68fb3ef19e
commit 171e7f0cb2
3 files changed
+48 -36

No files matched your search

@@ -44,10 +44,13 @@ function CredentialEditor({
const [showPassword, setShowPassword] = useState(false);
const usernamePromptRegex = entry.username_prompt_regex ?? "";
const passwordPromptRegex = entry.password_prompt_regex ?? "";
const usernameRegexValid = validatePromptRegex(usernamePromptRegex);
const passwordRegexValid = validatePromptRegex(passwordPromptRegex);
const regexError = (value: string) =>
value.trim() ? t("credentialManager.invalidRegex") : t("credentialManager.regexRequired");
const usernameRegexValid =
!usernamePromptRegex.trim() || validatePromptRegex(usernamePromptRegex);
const passwordRegexValid =
!passwordPromptRegex.trim() || validatePromptRegex(passwordPromptRegex);
const showUsernameRegexValid = Boolean(usernamePromptRegex.trim() && usernameRegexValid);
const showPasswordRegexValid = Boolean(passwordPromptRegex.trim() && passwordRegexValid);
const regexError = (value: string) => (value.trim() ? t("credentialManager.invalidRegex") : "");
return (
<div className="border-b bg-accent/25 p-3">
@@ -80,12 +83,12 @@ function CredentialEditor({
<div className="space-y-2 border-t pt-3">
<div className="flex items-center gap-1.5 text-xs font-medium">
<UserRound className="h-3.5 w-3.5 text-primary" />
{t("credentialManager.usernameLabel")}
{t("credentialManager.usernameOptionalLabel")}
</div>
<div className="space-y-2">
<div className="space-y-1.5">
<Label className="text-[0.6875rem] text-muted-foreground">
{t("credentialManager.promptRegexLabel")}
{t("credentialManager.promptRegexOptionalLabel")}
</Label>
<div className="relative">
<Input
@@ -95,7 +98,7 @@ function CredentialEditor({
onChange={(event) => onChange({ username_prompt_regex: event.target.value })}
aria-invalid={!usernameRegexValid}
/>
{usernameRegexValid ? (
{showUsernameRegexValid ? (
<CheckCircle2 className="pointer-events-none absolute top-1/2 right-2 h-3.5 w-3.5 -translate-y-1/2 text-emerald-500" />
) : null}
</div>
@@ -128,7 +131,7 @@ function CredentialEditor({
<div className="space-y-2">
<div className="space-y-1.5">
<Label className="text-[0.6875rem] text-muted-foreground">
{t("credentialManager.promptRegexLabel")}
{t("credentialManager.promptRegexOptionalLabel")}
</Label>
<div className="relative">
<Input
@@ -138,7 +141,7 @@ function CredentialEditor({
onChange={(event) => onChange({ password_prompt_regex: event.target.value })}
aria-invalid={!passwordRegexValid}
/>
{passwordRegexValid ? (
{showPasswordRegexValid ? (
<CheckCircle2 className="pointer-events-none absolute top-1/2 right-2 h-3.5 w-3.5 -translate-y-1/2 text-emerald-500" />
) : null}
</div>
@@ -322,16 +325,14 @@ export function CredentialManagementTab({
setEditEntry((prev) => ({ ...prev, ...patch }));
}, []);
const usernamePromptRegex = editEntry.username_prompt_regex ?? "";
const passwordPromptRegex = editEntry.password_prompt_regex ?? "";
const regexValid =
validatePromptRegex(editEntry.username_prompt_regex ?? "") &&
validatePromptRegex(editEntry.password_prompt_regex ?? "");
(!usernamePromptRegex.trim() || validatePromptRegex(usernamePromptRegex)) &&
(!passwordPromptRegex.trim() || validatePromptRegex(passwordPromptRegex));
const saveDisabled =
passwordLoading ||
!editEntry.name?.trim() ||
!editEntry.username?.trim() ||
(isNew && !editEntry.password) ||
!regexValid;
passwordLoading || !editEntry.name?.trim() || (isNew && !editEntry.password) || !regexValid;
const handleSave = useCallback(async () => {
if (saveDisabled) return;
@@ -427,7 +428,7 @@ export function CredentialManagementTab({
</div>
<div className="mt-1 flex items-start gap-0.5">
<span className="min-w-0 select-text break-all text-[0.6875rem] text-muted-foreground">
{entry.username}
{entry.username || t("credentialManager.passwordOnlyCredential")}
</span>
{entry.username ? <CopyButton value={entry.username} /> : null}
</div>
+15 -18
View File
@@ -4,8 +4,11 @@ import { useCallback, useEffect, useRef, useState } from "react";
import {
type CredentialPromptKind,
compilePromptRegex,
extractCredentialPromptText,
findMatchingCredentials,
findPasswordOnlyFallbackCredentials,
getCredentialPromptPattern,
isDefaultPasswordPrompt,
stripTerminalControlSequences,
} from "@/lib/credentialAutofill";
import { invoke } from "@/lib/invoke";
@@ -34,24 +37,10 @@ export interface CredentialPanelState {
promptText: string;
}
function extractCredentialPromptText(output: string) {
const lastChar = output[output.length - 1];
if (lastChar === "\r" || lastChar === "\n") return "";
const normalized = output.replace(/\r/g, "\n").replace(/\n+/g, "\n");
const lines = normalized.split("\n");
const prompt = (lines[lines.length - 1] ?? "").trim();
return prompt.length > 500 ? prompt.slice(-500) : prompt;
}
function extractCurrentPromptLine(promptText: string) {
return promptText.trim();
}
function looksLikePasswordPrompt(promptText: string) {
return /\b(pass(word|phrase|code)?|pin)\b/i.test(promptText);
}
function credentialPatternMatches(
credential: SavedCredential,
kind: CredentialPromptKind,
@@ -326,12 +315,11 @@ export function useCredentialAutofill(
return;
}
if (looksLikePasswordPrompt(currentLine)) {
if (isDefaultPasswordPrompt(currentLine)) {
pendingCredentialRef.current = null;
} else {
return;
}
return;
}
const passwordMatches = findMatchingCredentials(credentials, "password", promptText);
@@ -342,6 +330,16 @@ export function useCredentialAutofill(
return;
}
if (isDefaultPasswordPrompt(promptText)) {
const fallbackMatches = findPasswordOnlyFallbackCredentials(credentials);
if (fallbackMatches.length > 0) {
if (!rememberPrompt("password", promptText, now)) return;
showPanel("password", fallbackMatches, promptText);
return;
}
}
const usernameMatches = findMatchingCredentials(credentials, "username", promptText);
if (usernameMatches.length === 0) return;
if (!rememberPrompt("username", promptText, now)) return;
@@ -365,7 +363,6 @@ export function useCredentialAutofill(
(payload: string) => {
if (performanceModeRef.current === "overloaded") return;
if (!activeRef.current || !visibleRef.current) return;
if (credentialsRef.current.length === 0) return;
const visible = stripTerminalControlSequences(payload);
if (!visible) return;
+15 -1
View File
@@ -9,6 +9,7 @@ const USERNAME_PROMPT_PATTERN =
/\b(user\s*name|username|login|login\s+as|account|user)\b|(?:\u7528\u6237\u540d|\u7528\u6237|\u8d26\u53f7|\u8d26\u6237|\u767b\u5f55\u540d)/i;
const PASSWORD_PROMPT_PATTERN =
/\b(pass(word|phrase|code)?|pin|otp|verification\s*code|auth(entication)?\s*code|2fa|mfa)\b|(?:\u5bc6\u7801|\u53e3\u4ee4|\u9a8c\u8bc1\u7801|\u52a8\u6001\u7801|\u52a8\u6001\u53e3\u4ee4)/i;
const PROMPT_TERMINATOR_PATTERN = /[:\uff1a]\s*$/u;
export function stripTerminalControlSequences(text: string): string {
return text.replace(OSC_PATTERN, "").replace(ANSI_PATTERN, "");
@@ -27,12 +28,19 @@ export function extractCredentialPromptText(output: string): string {
export function detectCredentialPromptKind(output: string): CredentialPromptKind | null {
const prompt = extractCredentialPromptText(output);
if (!prompt || !/[:\uff1a]\s*$/u.test(prompt)) return null;
if (!prompt || !PROMPT_TERMINATOR_PATTERN.test(prompt)) return null;
if (PASSWORD_PROMPT_PATTERN.test(prompt)) return "password";
if (USERNAME_PROMPT_PATTERN.test(prompt)) return "username";
return null;
}
export function isDefaultPasswordPrompt(output: string): boolean {
const prompt = extractCredentialPromptText(output);
return Boolean(
prompt && PROMPT_TERMINATOR_PATTERN.test(prompt) && PASSWORD_PROMPT_PATTERN.test(prompt),
);
}
export function compilePromptRegex(pattern: string): RegExp | null {
try {
return new RegExp(pattern, "im");
@@ -74,6 +82,12 @@ export function findMatchingCredentials(
return credentials.filter((credential) => credentialMatchesPrompt(credential, kind, output));
}
export function findPasswordOnlyFallbackCredentials(
credentials: SavedCredential[],
): SavedCredential[] {
return credentials.filter((credential) => credential.enabled);
}
export function validatePromptRegex(pattern: string): boolean {
const trimmed = pattern.trim();
return Boolean(trimmed && compilePromptRegex(trimmed));