mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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:
co-authored by
npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757
parent
d52dedb06f
commit
8d3666c5f8
@@ -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'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'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'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 ? (
|
||||
|
||||
Reference in New Issue
Block a user