refactor: agent provider to use explicit type states (#8879)
This commit is contained in:
@@ -15,6 +15,8 @@ import {
|
|||||||
import type { ProviderDisplayInfo } from "@/shared/types/providers";
|
import type { ProviderDisplayInfo } from "@/shared/types/providers";
|
||||||
|
|
||||||
type SetupPhase = "idle" | "checking" | "installing" | "authenticating";
|
type SetupPhase = "idle" | "checking" | "installing" | "authenticating";
|
||||||
|
type InstallStatus = "checking" | "installed" | "missing";
|
||||||
|
type AuthStatus = "checking" | "authenticated" | "unauthenticated" | "unknown";
|
||||||
|
|
||||||
interface OutputLine {
|
interface OutputLine {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -29,11 +31,21 @@ interface AgentProviderCardProps {
|
|||||||
|
|
||||||
export function AgentProviderCard({ provider }: AgentProviderCardProps) {
|
export function AgentProviderCard({ provider }: AgentProviderCardProps) {
|
||||||
const { t } = useTranslation(["settings", "common"]);
|
const { t } = useTranslation(["settings", "common"]);
|
||||||
|
const isBuiltIn = provider.status === "built_in";
|
||||||
|
const hasInstallCommand = !!provider.installCommand;
|
||||||
|
const hasAuthCommand = !!provider.authCommand;
|
||||||
|
const hasBinary = !!provider.binaryName;
|
||||||
const [setupPhase, setSetupPhase] = useState<SetupPhase>("idle");
|
const [setupPhase, setSetupPhase] = useState<SetupPhase>("idle");
|
||||||
const [setupOutput, setSetupOutput] = useState<OutputLine[]>([]);
|
const [setupOutput, setSetupOutput] = useState<OutputLine[]>([]);
|
||||||
const [setupError, setSetupError] = useState<string | null>(null);
|
const [setupError, setSetupError] = useState<string | null>(null);
|
||||||
const [isInstalled, setIsInstalled] = useState<boolean | null>(null);
|
const [installStatus, setInstallStatus] = useState<InstallStatus>(
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
hasBinary && !isBuiltIn ? "checking" : "installed",
|
||||||
|
);
|
||||||
|
const [authStatus, setAuthStatus] = useState<AuthStatus>(
|
||||||
|
provider.authStatusCommand && hasBinary && !isBuiltIn
|
||||||
|
? "checking"
|
||||||
|
: "unknown",
|
||||||
|
);
|
||||||
const outputRef = useRef<HTMLDivElement>(null);
|
const outputRef = useRef<HTMLDivElement>(null);
|
||||||
const outputLengthRef = useRef(0);
|
const outputLengthRef = useRef(0);
|
||||||
const lineCounterRef = useRef(0);
|
const lineCounterRef = useRef(0);
|
||||||
@@ -41,12 +53,7 @@ export function AgentProviderCard({ provider }: AgentProviderCardProps) {
|
|||||||
const unlistenRef = useRef<(() => void) | null>(null);
|
const unlistenRef = useRef<(() => void) | null>(null);
|
||||||
|
|
||||||
const icon = getProviderIcon(provider.id, "size-6");
|
const icon = getProviderIcon(provider.id, "size-6");
|
||||||
const isBuiltIn = provider.status === "built_in";
|
|
||||||
const isActive = setupPhase !== "idle";
|
const isActive = setupPhase !== "idle";
|
||||||
const hasInstallCommand = !!provider.installCommand;
|
|
||||||
const hasAuthCommand = !!provider.authCommand;
|
|
||||||
const hasAuthStatusCheck = !!provider.authStatusCommand;
|
|
||||||
const hasBinary = !!provider.binaryName;
|
|
||||||
const authStorageKey = `agent-provider-auth:${provider.id}`;
|
const authStorageKey = `agent-provider-auth:${provider.id}`;
|
||||||
|
|
||||||
const setAuthHint = useCallback(
|
const setAuthHint = useCallback(
|
||||||
@@ -83,24 +90,25 @@ export function AgentProviderCard({ provider }: AgentProviderCardProps) {
|
|||||||
checkAgentInstalled(provider.id)
|
checkAgentInstalled(provider.id)
|
||||||
.then((installed) => {
|
.then((installed) => {
|
||||||
if (!isMountedRef.current) return;
|
if (!isMountedRef.current) return;
|
||||||
setIsInstalled(installed);
|
setInstallStatus(installed ? "installed" : "missing");
|
||||||
if (installed && provider.authStatusCommand) {
|
if (installed && provider.authStatusCommand) {
|
||||||
return checkAgentAuth(provider.id).then((authenticated) => {
|
return checkAgentAuth(provider.id).then((authenticated) => {
|
||||||
if (!isMountedRef.current) return;
|
if (!isMountedRef.current) return;
|
||||||
setIsAuthenticated(authenticated);
|
setAuthStatus(authenticated ? "authenticated" : "unauthenticated");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (installed && !provider.authStatusCommand) {
|
if (installed && !provider.authStatusCommand) {
|
||||||
setIsAuthenticated(getAuthHint() ? true : null);
|
setAuthStatus(getAuthHint() ? "authenticated" : "unknown");
|
||||||
}
|
}
|
||||||
if (!installed) {
|
if (!installed) {
|
||||||
setIsAuthenticated(null);
|
setAuthStatus("unknown");
|
||||||
setAuthHint(false);
|
setAuthHint(false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (!isMountedRef.current) return;
|
if (!isMountedRef.current) return;
|
||||||
setIsInstalled(false);
|
setInstallStatus("missing");
|
||||||
|
setAuthStatus("unknown");
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
getAuthHint,
|
getAuthHint,
|
||||||
@@ -135,7 +143,7 @@ export function AgentProviderCard({ provider }: AgentProviderCardProps) {
|
|||||||
setSetupOutput([]);
|
setSetupOutput([]);
|
||||||
lineCounterRef.current = 0;
|
lineCounterRef.current = 0;
|
||||||
|
|
||||||
if (hasInstallCommand && isInstalled === false) {
|
if (hasInstallCommand && installStatus === "missing") {
|
||||||
await runInstall();
|
await runInstall();
|
||||||
} else if (hasAuthCommand) {
|
} else if (hasAuthCommand) {
|
||||||
await runAuth();
|
await runAuth();
|
||||||
@@ -161,14 +169,13 @@ export function AgentProviderCard({ provider }: AgentProviderCardProps) {
|
|||||||
|
|
||||||
if (hasBinary && provider.binaryName) {
|
if (hasBinary && provider.binaryName) {
|
||||||
setSetupPhase("checking");
|
setSetupPhase("checking");
|
||||||
const installed = await checkAgentInstalled(provider.binaryName);
|
const installed = await checkAgentInstalled(provider.id);
|
||||||
if (!isMountedRef.current) return;
|
if (!isMountedRef.current) return;
|
||||||
setIsInstalled(installed);
|
setInstallStatus(installed ? "installed" : "missing");
|
||||||
if (!installed) {
|
if (!installed) {
|
||||||
|
setAuthStatus("unknown");
|
||||||
setAuthHint(false);
|
setAuthHint(false);
|
||||||
setSetupError(
|
setSetupError(t("providers.agents.errors.installVerificationFailed"));
|
||||||
"Install finished but the CLI was not found on PATH. You may need to restart your terminal.",
|
|
||||||
);
|
|
||||||
setSetupPhase("idle");
|
setSetupPhase("idle");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -206,7 +213,7 @@ export function AgentProviderCard({ provider }: AgentProviderCardProps) {
|
|||||||
clearListener();
|
clearListener();
|
||||||
if (!isMountedRef.current) return;
|
if (!isMountedRef.current) return;
|
||||||
setAuthHint(true);
|
setAuthHint(true);
|
||||||
setIsAuthenticated(true);
|
setAuthStatus("authenticated");
|
||||||
setSetupPhase("idle");
|
setSetupPhase("idle");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
clearListener();
|
clearListener();
|
||||||
@@ -221,15 +228,19 @@ export function AgentProviderCard({ provider }: AgentProviderCardProps) {
|
|||||||
void handleConnect();
|
void handleConnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider.showOnlyWhenInstalled && isInstalled !== true) return null;
|
if (provider.showOnlyWhenInstalled && installStatus !== "installed")
|
||||||
|
return null;
|
||||||
|
|
||||||
const isReady =
|
const isReady =
|
||||||
isBuiltIn ||
|
isBuiltIn ||
|
||||||
(isInstalled === true && !hasAuthCommand) ||
|
(installStatus === "installed" && !hasAuthCommand) ||
|
||||||
(isInstalled === true && isAuthenticated === true);
|
(installStatus === "installed" && authStatus === "authenticated");
|
||||||
const needsAuth =
|
const needsAuth =
|
||||||
isInstalled === true && hasAuthCommand && isAuthenticated !== true;
|
installStatus === "installed" &&
|
||||||
const needsInstall = isInstalled === false && hasInstallCommand;
|
hasAuthCommand &&
|
||||||
|
authStatus !== "checking" &&
|
||||||
|
authStatus !== "authenticated";
|
||||||
|
const needsInstall = installStatus === "missing" && hasInstallCommand;
|
||||||
|
|
||||||
function renderStatusIndicator() {
|
function renderStatusIndicator() {
|
||||||
if (isBuiltIn || isReady) {
|
if (isBuiltIn || isReady) {
|
||||||
@@ -272,7 +283,6 @@ export function AgentProviderCard({ provider }: AgentProviderCardProps) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-xs"
|
size="icon-xs"
|
||||||
onClick={() => void handleConnect()}
|
onClick={() => void handleConnect()}
|
||||||
disabled={isInstalled === null && hasBinary}
|
|
||||||
className="flex-shrink-0 text-muted-foreground"
|
className="flex-shrink-0 text-muted-foreground"
|
||||||
aria-label={t("providers.agents.installLabel", {
|
aria-label={t("providers.agents.installLabel", {
|
||||||
name: provider.displayName,
|
name: provider.displayName,
|
||||||
@@ -288,11 +298,12 @@ export function AgentProviderCard({ provider }: AgentProviderCardProps) {
|
|||||||
|
|
||||||
function renderStatusText() {
|
function renderStatusText() {
|
||||||
if (isBuiltIn || isReady) return null;
|
if (isBuiltIn || isReady) return null;
|
||||||
if (setupError) return "Setup failed";
|
if (setupError) return t("providers.agents.status.setupFailed");
|
||||||
|
|
||||||
if (isInstalled === null && hasBinary) return "Checking...";
|
if (installStatus === "checking" && hasBinary)
|
||||||
if (isInstalled === true && hasAuthStatusCheck && isAuthenticated === null)
|
return t("providers.agents.status.checking");
|
||||||
return "Checking...";
|
if (installStatus === "installed" && authStatus === "checking")
|
||||||
|
return t("providers.agents.status.checking");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -313,21 +324,38 @@ export function AgentProviderCard({ provider }: AgentProviderCardProps) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderSetupOutput(scrollToEnd = false) {
|
||||||
|
if (setupOutput.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={scrollToEnd ? outputRef : undefined}
|
||||||
|
className="max-h-24 overflow-y-auto rounded-md bg-muted px-2 py-1.5 font-mono text-xxs leading-relaxed text-muted-foreground"
|
||||||
|
>
|
||||||
|
{setupOutput.map((entry) => (
|
||||||
|
<div key={entry.id}>{entry.text || "\u00A0"}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function renderSetupProgress() {
|
function renderSetupProgress() {
|
||||||
if (!isActive) return null;
|
if (!isActive) return null;
|
||||||
|
|
||||||
const phaseLabel =
|
const phaseLabel =
|
||||||
setupPhase === "installing"
|
setupPhase === "installing"
|
||||||
? `Installing ${provider.displayName}...`
|
? t("providers.agents.progress.installing", {
|
||||||
|
name: provider.displayName,
|
||||||
|
})
|
||||||
: setupPhase === "authenticating"
|
: setupPhase === "authenticating"
|
||||||
? "Waiting for sign-in..."
|
? t("providers.waitingForSignIn")
|
||||||
: "Verifying installation...";
|
: t("providers.agents.progress.verifyingInstallation");
|
||||||
|
|
||||||
const stepInfo =
|
const stepInfo =
|
||||||
setupPhase === "installing" && hasAuthCommand
|
setupPhase === "installing" && hasAuthCommand
|
||||||
? "Step 1 of 2"
|
? t("providers.agents.progress.step", { step: 1, total: 2 })
|
||||||
: setupPhase === "authenticating" && hasInstallCommand
|
: setupPhase === "authenticating" && hasInstallCommand
|
||||||
? "Step 2 of 2"
|
? t("providers.agents.progress.step", { step: 2, total: 2 })
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -344,20 +372,14 @@ export function AgentProviderCard({ provider }: AgentProviderCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{setupOutput.length > 0 && (
|
{renderSetupOutput(true)}
|
||||||
<div
|
|
||||||
ref={outputRef}
|
|
||||||
className="max-h-24 overflow-y-auto rounded-md bg-muted px-2 py-1.5 font-mono text-xxs leading-relaxed text-muted-foreground"
|
|
||||||
>
|
|
||||||
{setupOutput.map((entry) => (
|
|
||||||
<div key={entry.id}>{entry.text || "\u00A0"}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const statusText = renderStatusText();
|
||||||
|
const action = renderAction();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -378,48 +400,37 @@ export function AgentProviderCard({ provider }: AgentProviderCardProps) {
|
|||||||
{renderStatusIndicator()}
|
{renderStatusIndicator()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(() => {
|
{(statusText || action) && (
|
||||||
const statusText = renderStatusText();
|
<div className="mt-3 flex items-center justify-between">
|
||||||
const action = renderAction();
|
<div className="flex items-center gap-1.5">
|
||||||
if (!statusText && !action) return null;
|
{statusText && (
|
||||||
return (
|
<>
|
||||||
<div className="mt-3 flex items-center justify-between">
|
<span
|
||||||
<div className="flex items-center gap-1.5">
|
className={cn(
|
||||||
{statusText && (
|
"size-1.5 rounded-full",
|
||||||
<>
|
setupError
|
||||||
<span
|
? "bg-danger"
|
||||||
className={cn(
|
: isActive
|
||||||
"size-1.5 rounded-full",
|
? "bg-accent animate-pulse"
|
||||||
setupError
|
: "bg-muted-foreground/40",
|
||||||
? "bg-danger"
|
)}
|
||||||
: isActive
|
/>
|
||||||
? "bg-accent animate-pulse"
|
<span className="text-xs text-muted-foreground">
|
||||||
: "bg-muted-foreground/40",
|
{statusText}
|
||||||
)}
|
</span>
|
||||||
/>
|
</>
|
||||||
<span className="text-xs text-muted-foreground">
|
)}
|
||||||
{statusText}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{action}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
{action}
|
||||||
})()}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{renderSetupProgress()}
|
{renderSetupProgress()}
|
||||||
|
|
||||||
{setupError && !isActive && (
|
{setupError && !isActive && (
|
||||||
<div className="mt-3 space-y-2 border-t pt-3">
|
<div className="mt-3 space-y-2 border-t pt-3">
|
||||||
<p className="text-xs text-danger">{setupError}</p>
|
<p className="text-xs text-danger">{setupError}</p>
|
||||||
{setupOutput.length > 0 && (
|
{renderSetupOutput()}
|
||||||
<div className="max-h-24 overflow-y-auto rounded-md bg-muted px-2 py-1.5 font-mono text-xxs leading-relaxed text-muted-foreground">
|
|
||||||
{setupOutput.map((entry) => (
|
|
||||||
<div key={entry.id}>{entry.text || "\u00A0"}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { act, screen, waitFor } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { renderWithProviders } from "@/test/render";
|
||||||
|
import { AgentProviderCard } from "../AgentProviderCard";
|
||||||
|
import type { ProviderDisplayInfo } from "@/shared/types/providers";
|
||||||
|
|
||||||
|
const checkAgentInstalled = vi.fn();
|
||||||
|
const checkAgentAuth = vi.fn();
|
||||||
|
const installAgent = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("@/features/providers/api/agentSetup", () => ({
|
||||||
|
checkAgentInstalled: (...args: unknown[]) => checkAgentInstalled(...args),
|
||||||
|
checkAgentAuth: (...args: unknown[]) => checkAgentAuth(...args),
|
||||||
|
installAgent: (...args: unknown[]) => installAgent(...args),
|
||||||
|
authenticateAgent: vi.fn(),
|
||||||
|
onAgentSetupOutput: vi.fn(async () => vi.fn()),
|
||||||
|
}));
|
||||||
|
|
||||||
|
function createProvider(): ProviderDisplayInfo {
|
||||||
|
return {
|
||||||
|
id: "claude-acp",
|
||||||
|
displayName: "Claude",
|
||||||
|
category: "agent",
|
||||||
|
description: "Claude provider",
|
||||||
|
setupMethod: "cli_auth",
|
||||||
|
binaryName: "claude",
|
||||||
|
authCommand: "claude auth login",
|
||||||
|
authStatusCommand: "claude auth status",
|
||||||
|
tier: "standard",
|
||||||
|
status: "not_installed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("AgentProviderCard", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not show sign in while auth status is checking", async () => {
|
||||||
|
let resolveAuth!: (authenticated: boolean) => void;
|
||||||
|
const authPromise = new Promise<boolean>((resolve) => {
|
||||||
|
resolveAuth = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
checkAgentInstalled.mockResolvedValue(true);
|
||||||
|
checkAgentAuth.mockReturnValue(authPromise);
|
||||||
|
|
||||||
|
renderWithProviders(<AgentProviderCard provider={createProvider()} />);
|
||||||
|
|
||||||
|
expect(await screen.findByText("Checking...")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByRole("button", { name: /sign in/i }),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
resolveAuth(false);
|
||||||
|
await authPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByRole("button", { name: /sign in/i }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("checks installation by provider id after installing", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
checkAgentInstalled
|
||||||
|
.mockResolvedValueOnce(false)
|
||||||
|
.mockResolvedValueOnce(true);
|
||||||
|
installAgent.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
renderWithProviders(
|
||||||
|
<AgentProviderCard
|
||||||
|
provider={{
|
||||||
|
...createProvider(),
|
||||||
|
authCommand: undefined,
|
||||||
|
authStatusCommand: undefined,
|
||||||
|
installCommand: "npm install -g claude-agent-acp",
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(
|
||||||
|
await screen.findByRole("button", { name: /install claude/i }),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(checkAgentInstalled).toHaveBeenNthCalledWith(2, "claude-acp");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -207,9 +207,21 @@
|
|||||||
"providers": {
|
"providers": {
|
||||||
"agents": {
|
"agents": {
|
||||||
"description": "Agents handle your requests using their own tools and models",
|
"description": "Agents handle your requests using their own tools and models",
|
||||||
|
"errors": {
|
||||||
|
"installVerificationFailed": "Install finished but the CLI was not found on PATH. You may need to restart your terminal."
|
||||||
|
},
|
||||||
"installLabel": "Install {{name}}",
|
"installLabel": "Install {{name}}",
|
||||||
|
"progress": {
|
||||||
|
"installing": "Installing {{name}}...",
|
||||||
|
"step": "Step {{step}} of {{total}}",
|
||||||
|
"verifyingInstallation": "Verifying installation..."
|
||||||
|
},
|
||||||
"signIn": "Sign in",
|
"signIn": "Sign in",
|
||||||
"signInLabel": "Sign in to {{name}}",
|
"signInLabel": "Sign in to {{name}}",
|
||||||
|
"status": {
|
||||||
|
"checking": "Checking...",
|
||||||
|
"setupFailed": "Setup failed"
|
||||||
|
},
|
||||||
"title": "Agents"
|
"title": "Agents"
|
||||||
},
|
},
|
||||||
"description": "Connect agents and AI models to use with Goose",
|
"description": "Connect agents and AI models to use with Goose",
|
||||||
|
|||||||
@@ -207,9 +207,21 @@
|
|||||||
"providers": {
|
"providers": {
|
||||||
"agents": {
|
"agents": {
|
||||||
"description": "Los agentes manejan tus solicitudes usando sus propias herramientas y modelos",
|
"description": "Los agentes manejan tus solicitudes usando sus propias herramientas y modelos",
|
||||||
|
"errors": {
|
||||||
|
"installVerificationFailed": "La instalación terminó, pero no se encontró la CLI en PATH. Puede que tengas que reiniciar tu terminal."
|
||||||
|
},
|
||||||
"installLabel": "Instalar {{name}}",
|
"installLabel": "Instalar {{name}}",
|
||||||
|
"progress": {
|
||||||
|
"installing": "Instalando {{name}}...",
|
||||||
|
"step": "Paso {{step}} de {{total}}",
|
||||||
|
"verifyingInstallation": "Verificando instalación..."
|
||||||
|
},
|
||||||
"signIn": "Iniciar sesión",
|
"signIn": "Iniciar sesión",
|
||||||
"signInLabel": "Iniciar sesión en {{name}}",
|
"signInLabel": "Iniciar sesión en {{name}}",
|
||||||
|
"status": {
|
||||||
|
"checking": "Comprobando...",
|
||||||
|
"setupFailed": "La configuración falló"
|
||||||
|
},
|
||||||
"title": "Agentes"
|
"title": "Agentes"
|
||||||
},
|
},
|
||||||
"description": "Conecta agentes y modelos de IA para usar con Goose",
|
"description": "Conecta agentes y modelos de IA para usar con Goose",
|
||||||
|
|||||||
Reference in New Issue
Block a user