Guide CLI installation and subscription sign-in (#1980)

Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Tyler
2026-07-16 14:21:03 -04:00
committed by GitHub
co-authored by npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757
parent d52dedb06f
commit 8d3666c5f8
13 changed files with 1130 additions and 70 deletions
+30
View File
@@ -1,6 +1,10 @@
import * as React from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
connectAcpRuntime,
discoverAcpAuthMethods,
} from "@/shared/api/tauriAgentAuth";
import {
attachManagedAgentToChannel,
createChannelManagedAgents,
@@ -101,6 +105,7 @@ export const managedAgentsQueryKey = ["managed-agents"] as const;
export const personasQueryKey = ["personas"] as const;
export const teamsQueryKey = ["teams"] as const;
export const acpRuntimesQueryKey = ["acp-runtimes"] as const;
export const acpAuthMethodsQueryKey = ["acp-auth-methods"] as const;
export const managedAgentPrereqsQueryKey = ["managed-agent-prereqs"] as const;
export const backendProvidersQueryKey = ["backend-providers"] as const;
export const gitBashPrerequisiteQueryKey = ["git-bash-prerequisite"] as const;
@@ -196,6 +201,31 @@ export function useAvailableAcpRuntimes(options?: { enabled?: boolean }) {
return { ...query, data: available };
}
export function useAcpAuthMethodsQuery(
runtimeId: string,
options?: { enabled?: boolean },
) {
return useQuery({
enabled: (options?.enabled ?? true) && runtimeId.trim().length > 0,
queryKey: [...acpAuthMethodsQueryKey, runtimeId],
queryFn: () => discoverAcpAuthMethods(runtimeId),
staleTime: 30_000,
});
}
export function useConnectAcpRuntimeMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (input: { runtimeId: string; methodId: string }) =>
connectAcpRuntime(input.runtimeId, input.methodId),
onSettled: () => {
void queryClient.invalidateQueries({ queryKey: acpRuntimesQueryKey });
void queryClient.invalidateQueries({ queryKey: acpAuthMethodsQueryKey });
void queryClient.invalidateQueries({ queryKey: managedAgentsQueryKey });
},
});
}
export function useInstallAcpRuntimeMutation() {
const queryClient = useQueryClient();
return useMutation({
@@ -11,12 +11,18 @@ import {
import { openUrl } from "@tauri-apps/plugin-opener";
import {
useAcpAuthMethodsQuery,
useAcpRuntimesQuery,
useInstallAcpRuntimeMutation,
useConnectAcpRuntimeMutation,
useGitBashPrerequisiteQuery,
useInstallAcpRuntimeMutation,
} from "@/features/agents/hooks";
import { describeResolvedCommand } from "@/features/agents/ui/agentUi";
import type { AcpRuntimeCatalogEntry, AuthStatus } from "@/shared/api/types";
import type {
AcpAuthMethod,
AcpRuntimeCatalogEntry,
AuthStatus,
} from "@/shared/api/types";
import { getInstallErrorMessage } from "@/shared/lib/installError";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
@@ -71,6 +77,125 @@ function AuthStatusBadge({ authStatus }: { authStatus: AuthStatus }) {
}
}
function AuthMethodButtonLabel({ method }: { method: AcpAuthMethod }) {
return <>{method.name || method.id}</>;
}
function ConnectAccountActions({
runtime,
}: {
runtime: AcpRuntimeCatalogEntry;
}) {
const authMethodsQuery = useAcpAuthMethodsQuery(runtime.id, {
enabled:
runtime.availability === "available" &&
runtime.authStatus.status === "logged_out",
});
const connectMutation = useConnectAcpRuntimeMutation();
const [terminalLaunchMethodId, setTerminalLaunchMethodId] = React.useState<
string | null
>(null);
if (runtime.authStatus.status !== "logged_out") {
return null;
}
const methods = authMethodsQuery.data?.methods ?? [];
const isConnecting = connectMutation.isPending;
function connect(method: AcpAuthMethod) {
connectMutation.mutate(
{ runtimeId: runtime.id, methodId: method.id },
{
onSuccess: (result) => {
if (result.launched && method.type === "terminal") {
setTerminalLaunchMethodId(method.id);
}
},
},
);
}
if (authMethodsQuery.isLoading) {
return (
<p className="mt-2 text-sm text-muted-foreground">
Looking for account connection options...
</p>
);
}
if (authMethodsQuery.error instanceof Error) {
return (
<p className="mt-2 rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-1.5 text-sm text-destructive">
Couldn&apos;t load account connection options:{" "}
{authMethodsQuery.error.message}
</p>
);
}
if (methods.length === 0) {
return (
<p className="mt-2 text-sm text-muted-foreground">
This adapter did not advertise a built-in login flow. Use the manual
instructions above, then click Re-run.
</p>
);
}
return (
<div className="mt-2 space-y-2">
<div className="flex flex-wrap items-center gap-2">
{methods.map((method) => {
const pending =
isConnecting && connectMutation.variables?.methodId === method.id;
return (
<Button
disabled={isConnecting}
key={method.id}
onClick={() => connect(method)}
size="sm"
type="button"
variant="outline"
>
{pending ? <RefreshCw className="h-4 w-4 animate-spin" /> : null}
{pending ? (
"Connecting..."
) : (
<AuthMethodButtonLabel method={method} />
)}
</Button>
);
})}
</div>
<p className="text-xs text-muted-foreground">
Buzz launches the adapter&apos;s own login flow and then re-checks the{" "}
{runtime.label} CLI. Credentials stay with {runtime.label}.
</p>
{terminalLaunchMethodId ? (
<p className="text-xs text-muted-foreground">
Finish signing in from the Terminal window, then click Re-run to
re-check {runtime.label}.
</p>
) : null}
{methods.map((method) =>
method.description ? (
<p className="text-xs text-muted-foreground" key={method.id}>
<span className="font-medium text-foreground/80">
{method.name || method.id}:
</span>{" "}
{method.description}
</p>
) : null,
)}
{connectMutation.error instanceof Error ? (
<p className="rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-1.5 text-sm text-destructive">
{connectMutation.error.message}
</p>
) : null}
</div>
);
}
function InstallActions({
hasError,
isInstalling,
@@ -83,35 +208,58 @@ function InstallActions({
runtime: AcpRuntimeCatalogEntry;
}) {
const showInstall = runtime.canAutoInstall && !runtime.nodeRequired;
const installLabel =
runtime.availability === "adapter_missing"
? "Install ACP adapter"
: runtime.availability === "adapter_outdated"
? "Update ACP adapter"
: `Install ${runtime.label}`;
const pendingLabel =
runtime.availability === "adapter_missing" ||
runtime.availability === "adapter_outdated"
? "Installing adapter..."
: `Installing ${runtime.label}...`;
return (
<div className="mt-2 flex items-center gap-2">
<div className="mt-2 space-y-2">
{showInstall ? (
<Button
disabled={isInstalling}
onClick={onInstall}
size="sm"
type="button"
variant="outline"
>
{isInstalling ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : hasError ? (
<RefreshCw className="h-4 w-4" />
) : (
<Download className="h-4 w-4" />
)}
{isInstalling ? "Installing..." : hasError ? "Retry" : "Install"}
</Button>
<p className="text-xs text-muted-foreground">
Buzz uses the official installer and adds the ACP adapter. After it
finishes, Buzz will show the vendor&apos;s sign-in flow here.
</p>
) : null}
<button
className="inline-flex items-center gap-1 text-xs text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
onClick={() => void openUrl(runtime.installInstructionsUrl)}
type="button"
>
<ExternalLink className="h-4 w-4" />
View instructions
</button>
<div className="flex items-center gap-2">
{showInstall ? (
<Button
disabled={isInstalling}
onClick={onInstall}
size="sm"
type="button"
variant="outline"
>
{isInstalling ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : hasError ? (
<RefreshCw className="h-4 w-4" />
) : (
<Download className="h-4 w-4" />
)}
{isInstalling
? pendingLabel
: hasError
? `Retry ${installLabel}`
: installLabel}
</Button>
) : null}
<button
className="inline-flex items-center gap-1 text-xs text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
onClick={() => void openUrl(runtime.installInstructionsUrl)}
type="button"
>
<ExternalLink className="h-4 w-4" />
View instructions
</button>
</div>
</div>
);
}
@@ -258,6 +406,7 @@ function RuntimeRow({
: runtime.loginHint}
</p>
) : null}
<ConnectAccountActions runtime={runtime} />
</>
) : runtime.availability === "adapter_missing" ? (
<>
@@ -348,7 +497,7 @@ function RuntimeRow({
{installSuccess && runtime.availability !== "available" ? (
<p className="mt-2 rounded-lg border border-green-500/30 bg-green-500/10 px-3 py-1.5 text-sm text-green-700 dark:text-green-400">
Installed successfully!
{runtime.label} installed. Checking for sign-in options...
</p>
) : null}
{installError ? (