[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
231
components/settings/tabs/ai/CursorSdkCard.tsx
Normal file
231
components/settings/tabs/ai/CursorSdkCard.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Check, Eye, EyeOff, RefreshCw } from "lucide-react";
|
||||
import { useI18n } from "../../../../application/i18n/I18nProvider";
|
||||
import { decryptField } from "../../../../infrastructure/persistence/secureFieldAdapter";
|
||||
import type { CursorAuthMode } from "../../../../infrastructure/ai/types";
|
||||
import { Button } from "../../../ui/button";
|
||||
import { cn } from "../../../../lib/utils";
|
||||
import { isCursorRuntimeInstalled, type AgentPathInfo } from "./types";
|
||||
|
||||
export const CursorSdkCard: React.FC<{
|
||||
pathInfo: AgentPathInfo | null;
|
||||
isResolvingPath: boolean;
|
||||
encryptedApiKey?: string;
|
||||
authMode: CursorAuthMode;
|
||||
onAuthModeChange: (mode: CursorAuthMode) => void;
|
||||
onSaveApiKey: (apiKey: string) => Promise<void>;
|
||||
onRecheckPath: () => void;
|
||||
}> = ({
|
||||
pathInfo,
|
||||
isResolvingPath,
|
||||
encryptedApiKey,
|
||||
authMode,
|
||||
onAuthModeChange,
|
||||
onSaveApiKey,
|
||||
onRecheckPath,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const [apiKeyDraft, setApiKeyDraft] = useState("");
|
||||
const [showApiKey, setShowApiKey] = useState(false);
|
||||
const [isDecrypting, setIsDecrypting] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setSaved(false);
|
||||
if (!encryptedApiKey) {
|
||||
setApiKeyDraft("");
|
||||
return;
|
||||
}
|
||||
setIsDecrypting(true);
|
||||
decryptField(encryptedApiKey)
|
||||
.then((value) => {
|
||||
if (!cancelled) setApiKeyDraft(value ?? "");
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setApiKeyDraft("");
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setIsDecrypting(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [encryptedApiKey]);
|
||||
|
||||
const installed = isCursorRuntimeInstalled(pathInfo);
|
||||
const hasStoredApiKey = Boolean(encryptedApiKey);
|
||||
const usesEnvApiKey = pathInfo?.authSource === "CURSOR_API_KEY" || (
|
||||
pathInfo?.apiKeyOk && !hasStoredApiKey && pathInfo?.authSource !== "settings"
|
||||
);
|
||||
// CLI login is only proven by the dedicated probe — never generic `authenticated`
|
||||
// (which is also true for env/settings API keys).
|
||||
const hasCliLogin = Boolean(
|
||||
pathInfo?.cliLoginOk || pathInfo?.authSource === "cli-login",
|
||||
);
|
||||
const hasAnyApiKey = hasStoredApiKey || Boolean(pathInfo?.apiKeyOk) || usesEnvApiKey
|
||||
|| pathInfo?.authSource === "settings"
|
||||
|| pathInfo?.authSource === "CURSOR_API_KEY";
|
||||
const isApiKeyMode = authMode === "api-key";
|
||||
const isCliMode = authMode === "cli-login";
|
||||
const available = isCliMode
|
||||
? hasCliLogin
|
||||
: (hasAnyApiKey && Boolean(pathInfo?.sdkInstalled ?? true));
|
||||
const canSave = isApiKeyMode && !isSaving && !isDecrypting && (Boolean(apiKeyDraft.trim()) || hasStoredApiKey);
|
||||
|
||||
const installStatus = isResolvingPath
|
||||
? t("ai.cursor.detecting")
|
||||
: installed
|
||||
? t("ai.cursor.installed")
|
||||
: t("ai.cursor.notInstalled");
|
||||
|
||||
const authStatus = isCliMode
|
||||
? hasCliLogin
|
||||
? (pathInfo?.cliEmail
|
||||
? t("ai.cursor.cliLoginAs", { email: pathInfo.cliEmail })
|
||||
: t("ai.cursor.cliLoginOk"))
|
||||
: t("ai.cursor.cliLoginMissing")
|
||||
: hasAnyApiKey
|
||||
? usesEnvApiKey && !hasStoredApiKey
|
||||
? t("ai.cursor.apiKeyFromEnv")
|
||||
: t("ai.cursor.apiKeyConfigured")
|
||||
: t("ai.cursor.apiKeyMissing");
|
||||
|
||||
const installStatusClassName = isResolvingPath
|
||||
? "text-muted-foreground"
|
||||
: installed
|
||||
? "text-emerald-500"
|
||||
: "text-amber-500";
|
||||
const authStatusClassName = isCliMode
|
||||
? (hasCliLogin ? "text-emerald-500" : "text-amber-500")
|
||||
: (hasAnyApiKey ? "text-emerald-500" : "text-amber-500");
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!isApiKeyMode) return;
|
||||
setIsSaving(true);
|
||||
setSaved(false);
|
||||
try {
|
||||
await onSaveApiKey(apiKeyDraft.trim());
|
||||
setSaved(true);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-4 space-y-3">
|
||||
<div className="flex gap-1 rounded-md border border-border/60 p-0.5 bg-muted/30">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onAuthModeChange("cli-login")}
|
||||
className={cn(
|
||||
"flex-1 h-7 rounded text-xs font-medium transition-colors",
|
||||
isCliMode ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{t("ai.cursor.modeCli")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onAuthModeChange("api-key")}
|
||||
className={cn(
|
||||
"flex-1 h-7 rounded text-xs font-medium transition-colors",
|
||||
isApiKeyMode ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{t("ai.cursor.modeApiKey")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-muted-foreground leading-4">
|
||||
{isCliMode ? t("ai.cursor.modeCliHint") : t("ai.cursor.modeApiKeyHint")}
|
||||
</p>
|
||||
|
||||
<div className="grid gap-2 text-xs">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-muted-foreground">{t("ai.cursor.installStatus")}</span>
|
||||
<span className={cn("font-medium", installStatusClassName)}>{installStatus}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-muted-foreground">
|
||||
{isCliMode ? t("ai.cursor.cliLoginStatus") : t("ai.cursor.apiKeyStatus")}
|
||||
</span>
|
||||
<span className={cn("font-medium truncate max-w-[60%] text-right", authStatusClassName)}>
|
||||
{authStatus}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!available && (
|
||||
<p className="text-xs text-amber-500">
|
||||
{isCliMode
|
||||
? t("ai.cursor.cliLoginHint")
|
||||
: (Boolean(pathInfo?.sdkInstalled) || installed)
|
||||
? t("ai.cursor.notFoundHint")
|
||||
: t("ai.cursor.notInstalledHint")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{isApiKeyMode ? (
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">{t("ai.cursor.apiKey")}</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-1">
|
||||
<input
|
||||
type={showApiKey ? "text" : "password"}
|
||||
value={isDecrypting ? "" : apiKeyDraft}
|
||||
onChange={(event) => {
|
||||
setSaved(false);
|
||||
setApiKeyDraft(event.target.value);
|
||||
}}
|
||||
placeholder={
|
||||
isDecrypting
|
||||
? t("ai.providers.apiKey.decrypting")
|
||||
: usesEnvApiKey && !hasStoredApiKey
|
||||
? t("ai.cursor.apiKeyPlaceholder.env")
|
||||
: t("ai.cursor.apiKeyPlaceholder")
|
||||
}
|
||||
disabled={isDecrypting}
|
||||
className="w-full h-8 rounded-md border border-input bg-background px-3 pr-9 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowApiKey((value) => !value)}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
aria-label={showApiKey ? t("ai.cursor.hideApiKey") : t("ai.cursor.showApiKey")}
|
||||
>
|
||||
{showApiKey ? <EyeOff size={14} /> : <Eye size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={handleSave} disabled={!canSave}>
|
||||
{saved ? <Check size={14} className="mr-1.5" /> : null}
|
||||
{saved ? t("ai.cursor.saved") : t("ai.cursor.saveApiKey")}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={onRecheckPath} disabled={isResolvingPath}>
|
||||
<RefreshCw size={14} className="mr-1.5" />
|
||||
{t("ai.cursor.check")}
|
||||
</Button>
|
||||
</div>
|
||||
{usesEnvApiKey && !hasStoredApiKey ? (
|
||||
<p className="text-[11px] text-muted-foreground leading-4">
|
||||
{t("ai.cursor.apiKeyEnvHint")}
|
||||
</p>
|
||||
) : null}
|
||||
{usesEnvApiKey && hasStoredApiKey ? (
|
||||
<p className="text-[11px] text-muted-foreground leading-4">
|
||||
{t("ai.cursor.apiKeyOverrideHint")}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex justify-end">
|
||||
<Button variant="outline" size="sm" onClick={onRecheckPath} disabled={isResolvingPath}>
|
||||
<RefreshCw size={14} className="mr-1.5" />
|
||||
{t("ai.cursor.check")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user