feat(FN-3396): bundle Cursor CLI as a plugin provider with dashboard auth w
Merges FN-3396's full Cursor CLI provider integration (Steps 1–4): defines a CLI-backed provider contract, adds the `fusion-plugin-cursor-runtime` plugin package with process management and runtime probes, wires dashboard auth flows and UI (ProviderCard, onboarding modal, settings), and bundles the Fusion-Task-Id: FN-3396
This commit is contained in:
@@ -1339,6 +1339,19 @@ export interface DroidCliStatus {
|
||||
ready: boolean;
|
||||
}
|
||||
|
||||
export interface CursorCliStatus {
|
||||
binary: {
|
||||
available: boolean;
|
||||
version?: string;
|
||||
binaryPath?: string;
|
||||
reason?: string;
|
||||
probeDurationMs: number;
|
||||
};
|
||||
enabled: boolean;
|
||||
extension: null;
|
||||
ready: boolean;
|
||||
}
|
||||
|
||||
export interface LlamaCppStatus {
|
||||
enabled: boolean;
|
||||
extension: {
|
||||
@@ -1407,6 +1420,10 @@ export function fetchDroidCliStatus(): Promise<DroidCliStatus> {
|
||||
return api<DroidCliStatus>("/providers/droid-cli/status");
|
||||
}
|
||||
|
||||
export function fetchCursorCliStatus(): Promise<CursorCliStatus> {
|
||||
return api<CursorCliStatus>("/providers/cursor-cli/status");
|
||||
}
|
||||
|
||||
/** Probe llama.cpp server + setting + extension state. */
|
||||
export function fetchLlamaCppStatus(): Promise<LlamaCppStatus> {
|
||||
return api<LlamaCppStatus>("/providers/llama-cpp/status");
|
||||
@@ -1671,6 +1688,15 @@ export function setDroidCliEnabled(
|
||||
});
|
||||
}
|
||||
|
||||
export function setCursorCliEnabled(
|
||||
enabled: boolean,
|
||||
): Promise<{ enabled: boolean; restartRequired: boolean }> {
|
||||
return api<{ enabled: boolean; restartRequired: boolean }>("/auth/cursor-cli", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ enabled }),
|
||||
});
|
||||
}
|
||||
|
||||
/** Enable or disable the llama.cpp provider. */
|
||||
export function setLlamaCppEnabled(
|
||||
enabled: boolean,
|
||||
|
||||
13
packages/dashboard/app/components/CursorCliProviderCard.css
Normal file
13
packages/dashboard/app/components/CursorCliProviderCard.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.cursor-cli-provider-card .auth-provider-cli-actions,
|
||||
.cursor-cli-provider-card .onboarding-provider-card__actions {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.cursor-cli-provider-card .auth-provider-cli-actions,
|
||||
.cursor-cli-provider-card .onboarding-provider-card__actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
115
packages/dashboard/app/components/CursorCliProviderCard.tsx
Normal file
115
packages/dashboard/app/components/CursorCliProviderCard.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { fetchCursorCliStatus, setCursorCliEnabled, type CursorCliStatus } from "../api";
|
||||
import { ProviderIcon } from "./ProviderIcon";
|
||||
import "./CursorCliProviderCard.css";
|
||||
|
||||
interface CursorCliProviderCardProps {
|
||||
authenticated: boolean;
|
||||
compact?: boolean;
|
||||
onToggled?: (nextEnabled: boolean) => void;
|
||||
}
|
||||
|
||||
export function CursorCliProviderCard({ authenticated, compact = false, onToggled }: CursorCliProviderCardProps) {
|
||||
const [status, setStatus] = useState<CursorCliStatus | null>(null);
|
||||
const [busy, setBusy] = useState<"enabling" | "disabling" | "testing" | null>(null);
|
||||
const mountedRef = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
const next = await fetchCursorCliStatus();
|
||||
if (mountedRef.current) setStatus(next);
|
||||
return next;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const handleToggle = useCallback(
|
||||
async (next: boolean) => {
|
||||
setBusy(next ? "enabling" : "disabling");
|
||||
try {
|
||||
const result = await setCursorCliEnabled(next);
|
||||
onToggled?.(result.enabled);
|
||||
await refresh();
|
||||
} finally {
|
||||
if (mountedRef.current) setBusy(null);
|
||||
}
|
||||
},
|
||||
[onToggled, refresh],
|
||||
);
|
||||
|
||||
const currentlyEnabled = status?.enabled ?? authenticated;
|
||||
const binaryAvailable = status?.binary.available ?? false;
|
||||
|
||||
const actions = (
|
||||
<>
|
||||
<button type="button" className="btn btn-sm" onClick={() => {
|
||||
setBusy("testing");
|
||||
void refresh().finally(() => {
|
||||
if (mountedRef.current) setBusy(null);
|
||||
});
|
||||
}} disabled={busy !== null}>
|
||||
{busy === "testing" ? <><Loader2 size={12} className="animate-spin" /> Testing…</> : "Test"}
|
||||
</button>
|
||||
{currentlyEnabled ? (
|
||||
<button type="button" className="btn btn-sm" onClick={() => void handleToggle(false)} disabled={busy !== null}>
|
||||
{busy === "disabling" ? "Disabling…" : "Disable"}
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" className="btn btn-primary btn-sm" onClick={() => void handleToggle(true)} disabled={busy !== null || !binaryAvailable}>
|
||||
{busy === "enabling" ? "Enabling…" : "Enable"}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const statusText = !status
|
||||
? "Probing local CLI…"
|
||||
: !status.binary.available
|
||||
? status.binary.reason ?? "`cursor-agent` not found on PATH"
|
||||
: currentlyEnabled
|
||||
? `Connected${status.binary.version ? ` — ${status.binary.version}` : ""}`
|
||||
: "Detected. Click Enable to route calls through Cursor CLI.";
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div className={`cursor-cli-provider-card auth-provider-card auth-provider-card--cli${authenticated ? " auth-provider-card--authenticated" : ""}`} data-testid="cursor-cli-provider-card">
|
||||
<div className="auth-provider-header">
|
||||
<div className="auth-provider-info">
|
||||
<ProviderIcon provider="cursor-cli" size="sm" />
|
||||
<strong>Cursor — via Cursor CLI</strong>
|
||||
<span className={`auth-status-badge ${currentlyEnabled ? "authenticated" : "not-authenticated"}`}>{currentlyEnabled ? "✓ Active" : "✗ Not connected"}</span>
|
||||
</div>
|
||||
<div className="auth-provider-cli-actions">{actions}</div>
|
||||
</div>
|
||||
<small className="settings-muted">{statusText}</small>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`cursor-cli-provider-card onboarding-provider-card${authenticated ? " onboarding-provider-card--connected" : ""}`} data-testid="cursor-cli-provider-card">
|
||||
<div className="onboarding-provider-card__icon">
|
||||
<ProviderIcon provider="cursor-cli" size="md" />
|
||||
</div>
|
||||
<div className="onboarding-provider-card__body">
|
||||
<strong className="onboarding-provider-card__name">Cursor — via Cursor CLI</strong>
|
||||
<span className="onboarding-provider-card__description">Route AI calls through your local Cursor agent runtime.</span>
|
||||
<small className="settings-muted">{statusText}</small>
|
||||
</div>
|
||||
<div className="onboarding-provider-card__actions">{actions}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import { useModalResizePersist } from "../hooks/useModalResizePersist";
|
||||
import { CustomModelDropdown } from "./CustomModelDropdown";
|
||||
import { ProviderIcon } from "./ProviderIcon";
|
||||
import { ClaudeCliProviderCard } from "./ClaudeCliProviderCard";
|
||||
import { CursorCliProviderCard } from "./CursorCliProviderCard";
|
||||
import { LlamaCppProviderCard } from "./LlamaCppProviderCard";
|
||||
import { LoginInstructions } from "./LoginInstructions";
|
||||
import { OAuthManualCodeForm } from "./OAuthManualCodeForm";
|
||||
@@ -199,6 +200,7 @@ const ONBOARDING_CURATED_PROVIDER_FAMILY_ORDER = [
|
||||
"anthropic",
|
||||
"claude-cli",
|
||||
"droid-cli",
|
||||
"cursor-cli",
|
||||
"llama-cpp",
|
||||
"openai-codex",
|
||||
"gemini",
|
||||
@@ -1783,6 +1785,18 @@ export function ModelOnboardingModal({
|
||||
);
|
||||
}
|
||||
|
||||
if (provider.id === "cursor-cli" && provider.type === "cli") {
|
||||
return (
|
||||
<CursorCliProviderCard
|
||||
key={provider.id}
|
||||
authenticated={provider.authenticated}
|
||||
onToggled={() => {
|
||||
void loadAuthStatus();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (provider.type === "api_key") {
|
||||
const providerInfo = getProviderInfo(provider.id);
|
||||
const apiKeyInfo = getApiKeyInfo(provider);
|
||||
|
||||
@@ -680,6 +680,38 @@ function DroidCliIcon({ size, color, label = "Factory AI — via Droid CLI" }: {
|
||||
);
|
||||
}
|
||||
|
||||
function CursorCliIcon({ size, color, label = "Cursor — via Cursor CLI" }: { size: number; color: string; label?: string }) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
data-testid="cursor-cli-icon"
|
||||
aria-label={label}
|
||||
>
|
||||
<rect x="2" y="3" width="14" height="14" rx="3" fill={color} />
|
||||
<path
|
||||
d="M10.8 7.2a3.6 3.6 0 1 0 0 5.6"
|
||||
stroke="var(--provider-icon-contrast)"
|
||||
strokeWidth="1.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<rect x="13" y="13" width="10" height="9" rx="1.5" fill={color} />
|
||||
<path
|
||||
d="M15.2 16.2l1.6 1.4-1.6 1.4M18.6 19.6h2.4"
|
||||
stroke="var(--provider-icon-contrast)"
|
||||
strokeWidth="1.2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const providerConfig: Record<
|
||||
string,
|
||||
{ component: typeof AnthropicIcon; color: string; label?: string }
|
||||
@@ -689,6 +721,7 @@ const providerConfig: Record<
|
||||
"claude-cli": { component: ClaudeCliIcon, color: "var(--provider-anthropic)", label: "Anthropic — via Claude CLI" },
|
||||
"pi-claude-cli": { component: ClaudeCliIcon, color: "var(--provider-anthropic)", label: "Anthropic — via Claude CLI" },
|
||||
"droid-cli": { component: DroidCliIcon, color: "var(--provider-openai)", label: "Factory AI — via Droid CLI" },
|
||||
"cursor-cli": { component: CursorCliIcon, color: "var(--provider-cursor-cli)", label: "Cursor — via Cursor CLI" },
|
||||
"llama-cpp": { component: LlamaCppIcon, color: "var(--provider-ollama)", label: "llama.cpp" },
|
||||
"llama-server": { component: LlamaCppIcon, color: "var(--provider-ollama)", label: "llama.cpp" },
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useModalResizePersist } from "../hooks/useModalResizePersist";
|
||||
const PluginManager = lazy(() => import("./PluginManager").then((m) => ({ default: m.PluginManager })));
|
||||
const PiExtensionsManager = lazy(() => import("./PiExtensionsManager").then((m) => ({ default: m.PiExtensionsManager })));
|
||||
import { ClaudeCliProviderCard } from "./ClaudeCliProviderCard";
|
||||
import { CursorCliProviderCard } from "./CursorCliProviderCard";
|
||||
import { CliBinaryPanel } from "./CliBinaryPanel";
|
||||
import { LlamaCppProviderCard } from "./LlamaCppProviderCard";
|
||||
import { HermesRuntimeCard } from "./HermesRuntimeCard";
|
||||
@@ -5074,6 +5075,7 @@ export function SettingsModal({
|
||||
// CLI-backed providers live in whichever bucket matches their current
|
||||
// auth state (Authenticated when signed in, Available otherwise).
|
||||
const claudeCliProvider = cliAuthProviders.find((p) => p.id === "claude-cli");
|
||||
const cursorCliProvider = cliAuthProviders.find((p) => p.id === "cursor-cli");
|
||||
const llamaCppProvider = cliAuthProviders.find((p) => p.id === "llama-cpp");
|
||||
const claudeCliCard = claudeCliProvider ? (
|
||||
<ClaudeCliProviderCard
|
||||
@@ -5084,6 +5086,15 @@ export function SettingsModal({
|
||||
}}
|
||||
/>
|
||||
) : null;
|
||||
const cursorCliCard = cursorCliProvider ? (
|
||||
<CursorCliProviderCard
|
||||
compact
|
||||
authenticated={cursorCliProvider.authenticated}
|
||||
onToggled={() => {
|
||||
void loadAuthStatus();
|
||||
}}
|
||||
/>
|
||||
) : null;
|
||||
const llamaCppCard = llamaCppProvider ? (
|
||||
<LlamaCppProviderCard
|
||||
compact
|
||||
@@ -5096,10 +5107,12 @@ export function SettingsModal({
|
||||
const showAuthenticatedGroup =
|
||||
authenticatedProviders.length > 0
|
||||
|| (claudeCliProvider?.authenticated ?? false)
|
||||
|| (cursorCliProvider?.authenticated ?? false)
|
||||
|| (llamaCppProvider?.authenticated ?? false);
|
||||
const showAvailableGroup =
|
||||
unauthenticatedProviders.length > 0
|
||||
|| (claudeCliProvider && !claudeCliProvider.authenticated)
|
||||
|| (cursorCliProvider && !cursorCliProvider.authenticated)
|
||||
|| (llamaCppProvider && !llamaCppProvider.authenticated);
|
||||
return (
|
||||
<>
|
||||
@@ -5133,6 +5146,7 @@ export function SettingsModal({
|
||||
<div className="auth-provider-group">
|
||||
<div className="auth-group-label">Authenticated</div>
|
||||
{claudeCliProvider?.authenticated && claudeCliCard}
|
||||
{cursorCliProvider?.authenticated && cursorCliCard}
|
||||
{llamaCppProvider?.authenticated && llamaCppCard}
|
||||
{authenticatedProviders.map((provider) => (
|
||||
<div key={provider.id} className="auth-provider-card auth-provider-card--authenticated">
|
||||
@@ -5227,6 +5241,7 @@ export function SettingsModal({
|
||||
<div className="auth-provider-group">
|
||||
<div className="auth-group-label">Available</div>
|
||||
{claudeCliProvider && !claudeCliProvider.authenticated && claudeCliCard}
|
||||
{cursorCliProvider && !cursorCliProvider.authenticated && cursorCliCard}
|
||||
{llamaCppProvider && !llamaCppProvider.authenticated && llamaCppCard}
|
||||
{unauthenticatedProviders.map((provider) => (
|
||||
<div key={provider.id} className="auth-provider-card">
|
||||
|
||||
@@ -19,6 +19,8 @@ const mockUpdateGlobalSettings = vi.fn();
|
||||
const mockCreateTask = vi.fn();
|
||||
const mockFetchCustomProviders = vi.fn();
|
||||
const mockCreateCustomProvider = vi.fn();
|
||||
const mockFetchCursorCliStatus = vi.fn();
|
||||
const mockSetCursorCliEnabled = vi.fn();
|
||||
|
||||
vi.mock("../../api", () => ({
|
||||
fetchAuthStatus: (...args: unknown[]) => mockFetchAuthStatus(...args),
|
||||
@@ -34,6 +36,8 @@ vi.mock("../../api", () => ({
|
||||
createTask: (...args: unknown[]) => mockCreateTask(...args),
|
||||
fetchCustomProviders: (...args: unknown[]) => mockFetchCustomProviders(...args),
|
||||
createCustomProvider: (...args: unknown[]) => mockCreateCustomProvider(...args),
|
||||
fetchCursorCliStatus: (...args: unknown[]) => mockFetchCursorCliStatus(...args),
|
||||
setCursorCliEnabled: (...args: unknown[]) => mockSetCursorCliEnabled(...args),
|
||||
}));
|
||||
|
||||
// Mock CustomModelDropdown since it has complex portal behavior
|
||||
@@ -188,6 +192,13 @@ beforeEach(() => {
|
||||
mockSubmitProviderManualCode.mockResolvedValue({ success: true, submitted: true });
|
||||
mockSaveApiKey.mockResolvedValue({ success: true });
|
||||
mockClearApiKey.mockResolvedValue({ success: true });
|
||||
mockFetchCursorCliStatus.mockResolvedValue({
|
||||
binary: { available: true, version: "0.1.0", binaryPath: "/usr/local/bin/cursor-agent", probeDurationMs: 8 },
|
||||
enabled: false,
|
||||
extension: null,
|
||||
ready: false,
|
||||
});
|
||||
mockSetCursorCliEnabled.mockResolvedValue({ enabled: true, restartRequired: false });
|
||||
// Default to no persisted state (start at ai-setup)
|
||||
mockGetOnboardingState.mockReturnValue(null);
|
||||
mockSaveOnboardingState.mockImplementation(() => {});
|
||||
@@ -906,6 +917,19 @@ describe("ModelOnboardingModal", () => {
|
||||
expect(description.closest(".onboarding-provider-card")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders cursor cli provider card when cursor provider is present", async () => {
|
||||
mockFetchAuthStatus.mockResolvedValue({
|
||||
providers: [
|
||||
{ id: "cursor-cli", name: "Cursor — via Cursor CLI", authenticated: true, type: "cli" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
|
||||
|
||||
expect(await screen.findByTestId("cursor-cli-provider-card")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cursor — via Cursor CLI")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders stable onboarding-provider-icon wrappers for provider cards", async () => {
|
||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
|
||||
|
||||
|
||||
@@ -58,6 +58,16 @@ describe("ProviderIcon", () => {
|
||||
expect(svg.parentElement).toHaveStyle({ color: "var(--provider-openai)" });
|
||||
});
|
||||
|
||||
it("renders cursor-cli icon with provider token color", () => {
|
||||
render(<ProviderIcon provider="cursor-cli" />);
|
||||
const svg = screen.getByTestId("cursor-cli-icon");
|
||||
expect(svg).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Cursor — via Cursor CLI")).toBeInTheDocument();
|
||||
const badgeGlyph = svg.querySelector('path[stroke="var(--provider-icon-contrast)"]');
|
||||
expect(badgeGlyph).toBeInTheDocument();
|
||||
expect(svg.parentElement).toHaveStyle({ color: "var(--provider-cursor-cli)" });
|
||||
});
|
||||
|
||||
it("normalizes PI-Claude-CLI provider name to lowercase alias", () => {
|
||||
render(<ProviderIcon provider="PI-Claude-CLI" />);
|
||||
const svg = screen.getByTestId("claude-cli-icon");
|
||||
|
||||
@@ -54,6 +54,8 @@ const mockTriggerMemoryDreams = vi.fn();
|
||||
const mockFetchPluginUiSlots = vi.fn();
|
||||
const mockFetchDroidCliStatus = vi.fn();
|
||||
const mockSetDroidCliEnabled = vi.fn();
|
||||
const mockFetchCursorCliStatus = vi.fn();
|
||||
const mockSetCursorCliEnabled = vi.fn();
|
||||
const mockUseWorkspaceFileBrowser = vi.fn();
|
||||
|
||||
vi.mock("../../api", async (importOriginal) => {
|
||||
@@ -107,6 +109,8 @@ vi.mock("../../api", async (importOriginal) => {
|
||||
fetchPluginUiSlots: (...args: unknown[]) => mockFetchPluginUiSlots(...args),
|
||||
fetchDroidCliStatus: (...args: unknown[]) => mockFetchDroidCliStatus(...args),
|
||||
setDroidCliEnabled: (...args: unknown[]) => mockSetDroidCliEnabled(...args),
|
||||
fetchCursorCliStatus: (...args: unknown[]) => mockFetchCursorCliStatus(...args),
|
||||
setCursorCliEnabled: (...args: unknown[]) => mockSetCursorCliEnabled(...args),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -377,6 +381,13 @@ describe("SettingsModal", () => {
|
||||
ready: false,
|
||||
});
|
||||
mockSetDroidCliEnabled.mockResolvedValue({ enabled: true, restartRequired: true });
|
||||
mockFetchCursorCliStatus.mockResolvedValue({
|
||||
binary: { available: true, version: "0.1.0", binaryPath: "/usr/local/bin/cursor-agent", probeDurationMs: 8 },
|
||||
enabled: false,
|
||||
extension: null,
|
||||
ready: false,
|
||||
});
|
||||
mockSetCursorCliEnabled.mockResolvedValue({ enabled: true, restartRequired: false });
|
||||
mockUseWorkspaceFileBrowser.mockReturnValue({
|
||||
entries: [],
|
||||
currentPath: ".",
|
||||
@@ -1188,6 +1199,37 @@ describe("SettingsModal", () => {
|
||||
expect(screen.getAllByTestId("droid-cli-provider-card")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("renders cursor cli auth card in authentication group", async () => {
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: [{ id: "cursor-cli", name: "Cursor — via Cursor CLI", authenticated: false, type: "cli" }],
|
||||
});
|
||||
mockFetchPluginUiSlots.mockResolvedValueOnce([]);
|
||||
|
||||
renderModal();
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
expect(await screen.findByTestId("cursor-cli-provider-card")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Enable" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("disables cursor enable action when binary is unavailable", async () => {
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: [{ id: "cursor-cli", name: "Cursor — via Cursor CLI", authenticated: false, type: "cli" }],
|
||||
});
|
||||
mockFetchCursorCliStatus.mockResolvedValueOnce({
|
||||
binary: { available: false, reason: "cursor-agent not found", probeDurationMs: 8 },
|
||||
enabled: false,
|
||||
extension: null,
|
||||
ready: false,
|
||||
});
|
||||
|
||||
renderModal();
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
expect(await screen.findByText("cursor-agent not found")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Enable" })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("does not render droid auth card when plugin slot is not present", async () => {
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: [{ id: "droid-cli", name: "Factory AI (via Droid CLI)", authenticated: false, type: "cli" }],
|
||||
|
||||
@@ -272,6 +272,7 @@ html {
|
||||
--provider-groq: #f55036;
|
||||
--provider-vercel: var(--text);
|
||||
--provider-droid-cli: var(--text);
|
||||
--provider-cursor-cli: #7c3aed;
|
||||
--provider-ollama: #d4a27f;
|
||||
/* Runtime-plugin marks. */
|
||||
--provider-hermes: #d4961c;
|
||||
|
||||
Reference in New Issue
Block a user