mirror of
https://github.com/block/buzz.git
synced 2026-11-13 21:01:53 +00:00
492 lines
12 KiB
TypeScript
492 lines
12 KiB
TypeScript
import type { LucideIcon } from "lucide-react";
|
|
import {
|
|
Activity,
|
|
Copy,
|
|
Cpu,
|
|
Fingerprint,
|
|
MessageSquare,
|
|
Server,
|
|
Terminal,
|
|
UserRound,
|
|
} from "lucide-react";
|
|
import * as React from "react";
|
|
import { toast } from "sonner";
|
|
|
|
import { AgentStatusBadge } from "@/features/agents/ui/AgentStatusBadge";
|
|
import { truncatePubkey as truncatePubkeyShort } from "@/features/profile/lib/identity";
|
|
import type {
|
|
AgentPersona,
|
|
ManagedAgent,
|
|
Profile,
|
|
RelayAgent,
|
|
} from "@/shared/api/types";
|
|
import { UserAvatar } from "@/shared/ui/UserAvatar";
|
|
|
|
const RUNTIME_LABELS: Record<string, string> = {
|
|
goose: "Goose",
|
|
"claude-code": "Claude Code",
|
|
"codex-acp": "Codex",
|
|
aider: "Aider",
|
|
};
|
|
|
|
function runtimeLabel(command: string): string {
|
|
return RUNTIME_LABELS[command] ?? command;
|
|
}
|
|
|
|
async function copyToClipboard(value: string, label?: string) {
|
|
await navigator.clipboard.writeText(value);
|
|
toast.success(label ? `Copied ${label}` : "Copied to clipboard");
|
|
}
|
|
|
|
export type ProfileField = {
|
|
copyValue?: string;
|
|
displayValue: string;
|
|
displayNode?: React.ReactNode;
|
|
icon: LucideIcon;
|
|
label: string;
|
|
testId?: string;
|
|
};
|
|
|
|
const AGENT_INFO_LABELS = new Set([
|
|
"Public key",
|
|
"Owned by",
|
|
"NIP-05",
|
|
"Agent type",
|
|
"Capabilities",
|
|
"Backend",
|
|
]);
|
|
const AGENT_SETTINGS_LABELS = new Set([
|
|
"Runtime",
|
|
"Respond to",
|
|
"ACP command",
|
|
"MCP command",
|
|
]);
|
|
const DIAGNOSTICS_LABELS = new Set(["Status", "Last error"]);
|
|
|
|
export function bucketProfileFields(fields: ProfileField[]) {
|
|
return {
|
|
agentInfoFields: fields.filter((field) =>
|
|
AGENT_INFO_LABELS.has(field.label),
|
|
),
|
|
agentSettingsFields: fields.filter((field) =>
|
|
AGENT_SETTINGS_LABELS.has(field.label),
|
|
),
|
|
diagnosticsFields: fields.filter((field) =>
|
|
DIAGNOSTICS_LABELS.has(field.label),
|
|
),
|
|
};
|
|
}
|
|
|
|
export function useProfileFieldBuckets({
|
|
isBot,
|
|
isOwner,
|
|
managedAgent,
|
|
ownerAvatarUrl,
|
|
ownerDisplayName,
|
|
ownerHandle,
|
|
ownerPubkey,
|
|
onOpenOwner,
|
|
persona,
|
|
presenceLoaded,
|
|
presenceStatus,
|
|
profile,
|
|
pubkey,
|
|
relayAgent,
|
|
}: {
|
|
isBot: boolean;
|
|
isOwner: boolean | undefined;
|
|
managedAgent: ManagedAgent | undefined;
|
|
ownerAvatarUrl: string | null;
|
|
ownerDisplayName: string | null;
|
|
ownerHandle: string | null;
|
|
ownerPubkey: string | null;
|
|
onOpenOwner?: () => void;
|
|
persona: AgentPersona | undefined;
|
|
presenceLoaded: boolean;
|
|
presenceStatus: "online" | "away" | "offline" | undefined;
|
|
profile: Profile | undefined;
|
|
pubkey: string | null;
|
|
relayAgent: RelayAgent | undefined;
|
|
}) {
|
|
return React.useMemo(() => {
|
|
const metadataFields = [
|
|
...buildPublicFields({ pubkey, profile, relayAgent, isBot, persona }),
|
|
...(isOwner === true
|
|
? buildOwnerFields({
|
|
managedAgent,
|
|
ownerAvatarUrl,
|
|
ownerDisplayName,
|
|
ownerHandle,
|
|
ownerPubkey,
|
|
onOpenOwner,
|
|
persona,
|
|
presenceLoaded,
|
|
presenceStatus,
|
|
relayAgent,
|
|
})
|
|
: []),
|
|
];
|
|
const diagnosticsFields =
|
|
bucketProfileFields(metadataFields).diagnosticsFields;
|
|
|
|
return {
|
|
...bucketProfileFields(metadataFields),
|
|
diagnosticsSummary:
|
|
diagnosticsFields.find((field) => field.label === "Status")
|
|
?.displayValue ??
|
|
diagnosticsFields.find((field) => field.label === "Last error")
|
|
?.displayValue ??
|
|
null,
|
|
modelLabel: managedAgent?.model ?? persona?.model ?? "Auto",
|
|
};
|
|
}, [
|
|
isBot,
|
|
isOwner,
|
|
managedAgent,
|
|
ownerAvatarUrl,
|
|
ownerDisplayName,
|
|
ownerHandle,
|
|
ownerPubkey,
|
|
onOpenOwner,
|
|
persona,
|
|
presenceLoaded,
|
|
presenceStatus,
|
|
profile,
|
|
pubkey,
|
|
relayAgent,
|
|
]);
|
|
}
|
|
|
|
export function buildPublicFields({
|
|
isBot,
|
|
persona,
|
|
profile,
|
|
pubkey,
|
|
relayAgent,
|
|
}: {
|
|
isBot: boolean;
|
|
persona?: AgentPersona;
|
|
profile: Profile | undefined;
|
|
pubkey: string | null;
|
|
relayAgent: RelayAgent | undefined;
|
|
}): ProfileField[] {
|
|
const fields: ProfileField[] = [];
|
|
|
|
if (pubkey) {
|
|
fields.push({
|
|
copyValue: pubkey,
|
|
displayValue: truncatePubkeyShort(pubkey),
|
|
icon: Fingerprint,
|
|
label: "Public key",
|
|
testId: "user-profile-copy-pubkey",
|
|
});
|
|
}
|
|
|
|
if (profile?.nip05Handle) {
|
|
fields.push({
|
|
copyValue: profile.nip05Handle,
|
|
displayValue: profile.nip05Handle,
|
|
icon: UserRound,
|
|
label: "NIP-05",
|
|
testId: "user-profile-nip05",
|
|
});
|
|
}
|
|
|
|
if (isBot && relayAgent?.agentType) {
|
|
fields.push({
|
|
copyValue: relayAgent.agentType,
|
|
displayValue: runtimeLabel(relayAgent.agentType),
|
|
icon: Cpu,
|
|
label: "Agent type",
|
|
testId: "user-profile-agent-type",
|
|
});
|
|
}
|
|
|
|
if (!pubkey && persona) {
|
|
fields.push({
|
|
displayValue: "Not deployed",
|
|
icon: Activity,
|
|
label: "Status",
|
|
testId: "user-profile-agent-status",
|
|
});
|
|
}
|
|
|
|
if (relayAgent?.capabilities.length) {
|
|
fields.push({
|
|
copyValue: relayAgent.capabilities.join(", "),
|
|
displayValue: relayAgent.capabilities.join(", "),
|
|
icon: Server,
|
|
label: "Capabilities",
|
|
testId: "user-profile-capabilities",
|
|
});
|
|
}
|
|
|
|
return fields;
|
|
}
|
|
|
|
export function buildOwnerFields({
|
|
managedAgent,
|
|
ownerAvatarUrl,
|
|
ownerDisplayName,
|
|
ownerHandle,
|
|
ownerPubkey,
|
|
onOpenOwner,
|
|
persona,
|
|
presenceLoaded,
|
|
presenceStatus,
|
|
relayAgent,
|
|
}: {
|
|
managedAgent: ManagedAgent | undefined;
|
|
ownerAvatarUrl: string | null;
|
|
ownerDisplayName: string | null;
|
|
ownerHandle: string | null;
|
|
ownerPubkey: string | null;
|
|
onOpenOwner?: () => void;
|
|
persona?: AgentPersona;
|
|
presenceLoaded: boolean;
|
|
presenceStatus: "online" | "away" | "offline" | undefined;
|
|
relayAgent: RelayAgent | undefined;
|
|
}): ProfileField[] {
|
|
const fields: ProfileField[] = [];
|
|
|
|
if (ownerDisplayName) {
|
|
fields.push({
|
|
copyValue: onOpenOwner
|
|
? undefined
|
|
: (ownerPubkey ?? ownerHandle ?? undefined),
|
|
displayValue: ownerDisplayName,
|
|
displayNode: onOpenOwner ? (
|
|
<button
|
|
className="inline-flex max-w-full items-center gap-2 rounded text-left text-sm text-muted-foreground transition-colors hover:text-foreground hover:underline focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
onOpenOwner();
|
|
}}
|
|
title={ownerDisplayName}
|
|
type="button"
|
|
>
|
|
<UserAvatar
|
|
avatarUrl={ownerAvatarUrl}
|
|
className="shrink-0"
|
|
displayName={ownerHandle ?? ownerDisplayName}
|
|
size="xs"
|
|
testId="user-profile-owner-avatar"
|
|
/>
|
|
<span className="truncate">{ownerDisplayName}</span>
|
|
</button>
|
|
) : undefined,
|
|
icon: UserRound,
|
|
label: "Owned by",
|
|
testId: "user-profile-owned-by",
|
|
});
|
|
}
|
|
|
|
if (managedAgent?.agentCommand) {
|
|
fields.push({
|
|
copyValue: managedAgent.agentCommand,
|
|
displayValue: runtimeLabel(managedAgent.agentCommand),
|
|
icon: Terminal,
|
|
label: "Runtime",
|
|
testId: "user-profile-runtime",
|
|
});
|
|
} else if (relayAgent?.agentType) {
|
|
fields.push({
|
|
copyValue: relayAgent.agentType,
|
|
displayValue: runtimeLabel(relayAgent.agentType),
|
|
icon: Terminal,
|
|
label: "Runtime",
|
|
testId: "user-profile-runtime",
|
|
});
|
|
} else if (persona?.runtime) {
|
|
fields.push({
|
|
copyValue: persona.runtime,
|
|
displayValue: runtimeLabel(persona.runtime),
|
|
icon: Terminal,
|
|
label: "Runtime",
|
|
testId: "user-profile-runtime",
|
|
});
|
|
}
|
|
|
|
if (managedAgent) {
|
|
fields.push({
|
|
displayValue: managedAgent.status
|
|
.replace(/_/g, " ")
|
|
.replace(/\b\w/g, (char: string) => char.toUpperCase()),
|
|
displayNode: (
|
|
<AgentStatusBadge
|
|
presenceLoaded={presenceLoaded}
|
|
presenceStatus={presenceStatus}
|
|
status={managedAgent.status}
|
|
/>
|
|
),
|
|
icon: Activity,
|
|
label: "Status",
|
|
testId: "user-profile-agent-status",
|
|
});
|
|
}
|
|
|
|
if (managedAgent?.model) {
|
|
fields.push({
|
|
copyValue: managedAgent.model,
|
|
displayValue: managedAgent.model,
|
|
icon: Cpu,
|
|
label: "Model",
|
|
testId: "user-profile-model",
|
|
});
|
|
} else if (persona?.model) {
|
|
fields.push({
|
|
copyValue: persona.model,
|
|
displayValue: persona.model,
|
|
icon: Cpu,
|
|
label: "Model",
|
|
testId: "user-profile-model",
|
|
});
|
|
}
|
|
|
|
if (managedAgent?.acpCommand) {
|
|
fields.push({
|
|
copyValue: managedAgent.acpCommand,
|
|
displayValue: managedAgent.acpCommand,
|
|
icon: Terminal,
|
|
label: "ACP command",
|
|
testId: "user-profile-acp",
|
|
});
|
|
}
|
|
|
|
if (managedAgent?.mcpCommand) {
|
|
fields.push({
|
|
copyValue: managedAgent.mcpCommand,
|
|
displayValue: managedAgent.mcpCommand,
|
|
icon: Terminal,
|
|
label: "MCP command",
|
|
testId: "user-profile-mcp",
|
|
});
|
|
}
|
|
|
|
if (managedAgent?.backend.type === "provider") {
|
|
const backendLabel = managedAgent.backend.id;
|
|
fields.push({
|
|
copyValue: backendLabel,
|
|
displayValue: backendLabel,
|
|
icon: Server,
|
|
label: "Backend",
|
|
testId: "user-profile-backend",
|
|
});
|
|
}
|
|
|
|
if (managedAgent) {
|
|
fields.push({
|
|
displayValue: managedAgent.startOnAppLaunch ? "Yes" : "No",
|
|
icon: Server,
|
|
label: "Start on launch",
|
|
testId: "user-profile-start-on-launch",
|
|
});
|
|
fields.push({
|
|
displayValue: managedAgent.respondTo.replace(/-/g, " "),
|
|
icon: MessageSquare,
|
|
label: "Respond to",
|
|
testId: "user-profile-respond-to",
|
|
});
|
|
}
|
|
|
|
if (managedAgent?.lastError) {
|
|
fields.push({
|
|
copyValue: managedAgent.lastError,
|
|
displayValue: managedAgent.lastError,
|
|
icon: Activity,
|
|
label: "Last error",
|
|
testId: "user-profile-last-error",
|
|
});
|
|
}
|
|
|
|
return fields;
|
|
}
|
|
|
|
export function ProfileFieldGroup({ fields }: { fields: ProfileField[] }) {
|
|
const publicKeyLabel = "Public key";
|
|
const ownedByLabel = "Owned by";
|
|
const statusLabel = "Status";
|
|
const orderedFields = [
|
|
...fields.filter((field) => field.label === publicKeyLabel),
|
|
...fields.filter((field) => field.label === ownedByLabel),
|
|
...fields.filter(
|
|
(field) =>
|
|
field.label !== publicKeyLabel &&
|
|
field.label !== ownedByLabel &&
|
|
field.copyValue,
|
|
),
|
|
...fields.filter((field) => field.label === statusLabel),
|
|
...fields.filter((field) => {
|
|
if (
|
|
field.label === publicKeyLabel ||
|
|
field.label === ownedByLabel ||
|
|
field.label === statusLabel
|
|
) {
|
|
return false;
|
|
}
|
|
return !field.copyValue;
|
|
}),
|
|
];
|
|
|
|
return (
|
|
<section>
|
|
<div className="overflow-hidden rounded-2xl bg-muted/20">
|
|
{orderedFields.map((field) => (
|
|
<ProfileFieldRow field={field} key={field.testId ?? field.label} />
|
|
))}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function ProfileFieldRow({ field }: { field: ProfileField }) {
|
|
const Icon = field.icon;
|
|
const isCopyable = Boolean(field.copyValue);
|
|
|
|
const content = (
|
|
<>
|
|
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-muted/60">
|
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
|
</span>
|
|
<span className="min-w-0 flex-1 text-left">
|
|
<span className="block text-xs font-medium text-foreground">
|
|
{field.label}
|
|
</span>
|
|
<span
|
|
className="mt-0.5 block truncate text-sm text-muted-foreground"
|
|
title={field.displayValue}
|
|
>
|
|
{field.displayNode ?? field.displayValue}
|
|
</span>
|
|
</span>
|
|
{isCopyable ? (
|
|
<Copy className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
) : null}
|
|
</>
|
|
);
|
|
|
|
if (isCopyable && field.copyValue) {
|
|
return (
|
|
<button
|
|
aria-label={`Copy ${field.label}`}
|
|
className="flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-muted/40"
|
|
data-testid={field.testId}
|
|
onClick={() => void copyToClipboard(field.copyValue ?? "", field.label)}
|
|
title={`Copy ${field.label}`}
|
|
type="button"
|
|
>
|
|
{content}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="flex items-center gap-3 px-4 py-3"
|
|
data-testid={field.testId}
|
|
>
|
|
{content}
|
|
</div>
|
|
);
|
|
}
|