diff --git a/packages/core/agents/index.ts b/packages/core/agents/index.ts index 322eedb3d5..c689d569e5 100644 --- a/packages/core/agents/index.ts +++ b/packages/core/agents/index.ts @@ -4,7 +4,6 @@ export * from "./failure-reason"; export * from "./effective-access"; export * from "./queries"; export * from "./use-agent-presence"; -export * from "./use-agent-runtime"; export * from "./use-update-agent-allowlist"; export * from "./use-agent-activity"; export * from "./use-workspace-presence-prefetch"; diff --git a/packages/core/agents/use-agent-runtime.ts b/packages/core/agents/use-agent-runtime.ts deleted file mode 100644 index 021746a394..0000000000 --- a/packages/core/agents/use-agent-runtime.ts +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; - -import { useMemo } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { agentListOptions } from "../workspace/queries"; -import { runtimeListOptions } from "../runtimes/queries"; - -/** - * The provider slug ("claude", "codex", …) backing an agent's runtime, or - * `null` when it can't be resolved — lists still loading, an agent outside the - * workspace list (archived assignee on an old issue), a runtime that no longer - * exists, or a backend that omitted the field. Callers render nothing on null - * rather than a placeholder: a wrong provider mark is worse than none. - * - * Derived from `runtime_id` on every read instead of stored on the agent, so - * moving an agent from one runtime to another moves its provider mark with it. - * - * Both queries are ones the surfaces that show a provider mark already - * subscribe to (`useActorName`, presence), so this costs a cache read rather - * than a request. - */ -export function useAgentRuntimeProvider( - wsId: string | undefined, - agentId: string | undefined, -): string | null { - const { data: agents } = useQuery({ - ...agentListOptions(wsId ?? ""), - enabled: !!wsId, - }); - const { data: runtimes } = useQuery({ - ...runtimeListOptions(wsId ?? ""), - enabled: !!wsId, - }); - - return useMemo(() => { - if (!agentId || !agents || !runtimes) return null; - const agent = agents.find((a) => a.id === agentId); - if (!agent) return null; - const runtime = runtimes.find((r) => r.id === agent.runtime_id); - return runtime?.provider?.trim() || null; - }, [agentId, agents, runtimes]); -} diff --git a/packages/views/agents/components/agent-detail-page.tsx b/packages/views/agents/components/agent-detail-page.tsx index 63c670f4b8..3e565c225d 100644 --- a/packages/views/agents/components/agent-detail-page.tsx +++ b/packages/views/agents/components/agent-detail-page.tsx @@ -430,7 +430,6 @@ function DetailHeader({ actorId={agent.id} size="2xl" profileLink={false} - showRuntimeBadge className="ring-1 ring-border" />
diff --git a/packages/views/agents/components/agent-profile-card.test.tsx b/packages/views/agents/components/agent-profile-card.test.tsx index c77668bc67..1a9f6fda52 100644 --- a/packages/views/agents/components/agent-profile-card.test.tsx +++ b/packages/views/agents/components/agent-profile-card.test.tsx @@ -16,8 +16,6 @@ vi.mock("@multica/core/paths", () => ({ useWorkspacePaths: () => ({ agentDetail: (id: string) => `/test/agents/${id}`, }), - // The runtime badge on the card's avatar resolves the provider per workspace. - useCurrentWorkspace: () => ({ id: "ws-1", slug: "test" }), })); vi.mock("@multica/core/api", () => ({ diff --git a/packages/views/agents/components/agent-profile-card.tsx b/packages/views/agents/components/agent-profile-card.tsx index b265e69a69..c20ac185cf 100644 --- a/packages/views/agents/components/agent-profile-card.tsx +++ b/packages/views/agents/components/agent-profile-card.tsx @@ -16,7 +16,6 @@ import { useWorkspacePaths } from "@multica/core/paths"; import { ActorAvatar as ActorAvatarBase } from "@multica/ui/components/common/actor-avatar"; import { Skeleton } from "@multica/ui/components/ui/skeleton"; import { AppLink } from "../../navigation"; -import { AgentRuntimeBadge } from "../../common/agent-runtime-badge"; import { HealthIcon } from "../../runtimes/components/shared"; import { availabilityConfig } from "../presence"; import { VisibilityBadge } from "./visibility-badge"; @@ -76,19 +75,16 @@ export function AgentProfileCard({ agentId }: AgentProfileCardProps) { availability dot is surfaced here; last-task state lives in the agents list and the agent detail page. */}
- {/* Base avatar + runtime badge rather than the ActorAvatar wrapper: - this card IS a hover-card payload, so it must not nest another - hover card or profile link inside itself. */} - - - - + {/* Base avatar rather than the ActorAvatar wrapper: this card IS a + hover-card payload, so it must not nest another hover card or + profile link inside itself. */} +

{agent.name}

diff --git a/packages/views/chat/components/chat-session-header.tsx b/packages/views/chat/components/chat-session-header.tsx index aab8366808..d4be8d87ff 100644 --- a/packages/views/chat/components/chat-session-header.tsx +++ b/packages/views/chat/components/chat-session-header.tsx @@ -105,14 +105,7 @@ export function ChatSessionHeader({ return (
{agent ? ( - + ) : ( )} diff --git a/packages/views/common/actor-avatar.tsx b/packages/views/common/actor-avatar.tsx index 3a088d97e5..59a455add5 100644 --- a/packages/views/common/actor-avatar.tsx +++ b/packages/views/common/actor-avatar.tsx @@ -11,10 +11,6 @@ import { import { useActorName } from "@multica/core/workspace/hooks"; import { useAgentPresenceDetail } from "@multica/core/agents"; import { useCurrentWorkspace, useWorkspacePaths } from "@multica/core/paths"; -import { - AgentRuntimeBadge, - RUNTIME_BADGE_MIN_AVATAR_PX, -} from "./agent-runtime-badge"; import { AgentProfileCard } from "../agents/components/agent-profile-card"; import { AgentLivePeekCard } from "../agents/components/agent-live-peek-card"; import { MemberProfileCard } from "../members/member-profile-card"; @@ -55,18 +51,6 @@ interface ActorAvatarProps { * popover inside the dropdown. */ showStatusDot?: boolean; - /** - * Overlay the provider mark of the agent's runtime at the avatar's top-right. - * Use on identity/config surfaces — the agent's own page, its profile card, - * a chat session header — where "what is running underneath this?" is a live - * question. Has no effect for non-agent actors. - * - * Independent of `showStatusDot`, and the two are designed to coexist: they - * sit at opposite ends of the circle and answer different questions (can it - * take work right now, vs what backs it). No-ops below - * {@link RUNTIME_BADGE_MIN_AVATAR_PX} — see that constant for why. - */ - showRuntimeBadge?: boolean; /** * When `enableHoverCard` is on for an agent, choose which payload to * render. See {@link AgentHoverCardVariant}. Defaults to `"profile"` so @@ -92,7 +76,6 @@ export function ActorAvatar({ className, enableHoverCard, showStatusDot, - showRuntimeBadge, hoverCardVariant = "profile", profileLink, }: ActorAvatarProps) { @@ -111,23 +94,19 @@ export function ActorAvatar({ /> ); - // Optional overlays. Only meaningful for agents — members have no presence - // backbone and no runtime. Wrapping unconditionally with relative inline-flex - // would create extra DOM for every avatar; we only wrap when an overlay is + // Optional presence overlay. Only meaningful for agents — members have no + // presence backbone. Wrapping unconditionally with relative inline-flex + // would create extra DOM for every avatar; we only wrap when the dot is // asked for. - const isAgent = actorType === "agent"; - const wrapDot = showStatusDot && isAgent; - const wrapBadge = showRuntimeBadge && isAgent; - const dotted = - wrapDot || wrapBadge ? ( - - {avatar} - {wrapBadge && } - {wrapDot && } - - ) : ( - avatar - ); + const wrapDot = showStatusDot && actorType === "agent"; + const dotted = wrapDot ? ( + + {avatar} + + + ) : ( + avatar + ); const shouldLinkToProfile = profileLink ?? (actorType === "member" || actorType === "agent" || actorType === "squad"); @@ -237,7 +216,6 @@ export function AgentStatusDot({ agentId, size }: { agentId: string; size?: Avat ); } - /** * Wraps an agent avatar in a hover-card. The trigger is keyboard-focusable * only when no focusable ancestor (link/button) already provides a tab stop — diff --git a/packages/views/common/agent-runtime-badge.test.tsx b/packages/views/common/agent-runtime-badge.test.tsx deleted file mode 100644 index 21d04f06d0..0000000000 --- a/packages/views/common/agent-runtime-badge.test.tsx +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Runtime provider badge on agent avatars. - * - * The badge and the presence dot are two overlays on one circle, so the rules - * that keep them from fighting each other are the ones worth pinning: the - * badge only appears where a provider mark is actually legible, it never - * displaces the dot, and it stays away from anything it can't resolve. - */ -import type { ReactElement } from "react"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { NavigationProvider } from "../navigation/context"; -import type { NavigationAdapter } from "../navigation/types"; - -const provider = vi.hoisted(() => ({ current: "claude" as string | null })); - -vi.mock("@multica/core/workspace/hooks", () => ({ - useActorName: () => ({ - getActorName: () => "Lambda", - getActorInitials: () => "L", - getActorAvatarUrl: () => null, - }), -})); - -vi.mock("@multica/core/paths", () => ({ - useWorkspacePaths: () => ({ - memberDetail: (id: string) => `/acme/members/${id}`, - agentDetail: (id: string) => `/acme/agents/${id}`, - squadDetail: (id: string) => `/acme/squads/${id}`, - }), - useCurrentWorkspace: () => ({ id: "ws1", slug: "acme" }), -})); - -vi.mock("@multica/core/agents", () => ({ - useAgentPresenceDetail: () => ({ availability: "online", workload: "idle" }), - useAgentRuntimeProvider: () => provider.current, -})); - -vi.mock("../agents/components/agent-profile-card", () => ({ - AgentProfileCard: () => null, -})); -vi.mock("../agents/components/agent-live-peek-card", () => ({ - AgentLivePeekCard: () => null, -})); -vi.mock("../members/member-profile-card", () => ({ - MemberProfileCard: () => null, -})); -vi.mock("../squads/components/squad-profile-card", () => ({ - SquadProfileCard: () => null, -})); - -import { ActorAvatar } from "./actor-avatar"; - -const AGENT_ID = "8f14e45f-ceea-4d0e-a1a2-9b1c0d3e4f5a"; - -// The avatar wraps itself in a profile link, which needs the adapter. -const ADAPTER: NavigationAdapter = { - push: vi.fn(), - replace: vi.fn(), - back: vi.fn(), - pathname: "/", - searchParams: new URLSearchParams(), - getShareableUrl: (p) => `https://app.example${p}`, -}; - -function renderAvatar(ui: ReactElement) { - return render({ui}); -} - -afterEach(() => { - cleanup(); - provider.current = "claude"; -}); - -describe("AgentRuntimeBadge", () => { - it("marks the avatar with the runtime's provider", () => { - renderAvatar(); - expect(screen.getByLabelText("Runtime: Claude")).toBeInTheDocument(); - }); - - // Provider marks are detailed artwork; at the ~8px a badge gets on a 20px - // picker row they're a smudge. That size is the status dot's home turf, so - // the badge has to stay out of it even when a caller asks for one. - it("stays off avatars too small to render a legible mark", () => { - renderAvatar(); - expect(screen.queryByLabelText("Runtime: Claude")).not.toBeInTheDocument(); - }); - - it("renders both overlays when a surface asks for the dot too", () => { - renderAvatar( - , - ); - expect(screen.getByLabelText("Runtime: Claude")).toBeInTheDocument(); - expect(screen.getByLabelText("Status: Online")).toBeInTheDocument(); - }); - - // Archived agent, deleted runtime, or a backend that omitted the field. A - // mark that names the wrong runtime is worse than no mark. - it("renders nothing when the provider cannot be resolved", () => { - provider.current = null; - renderAvatar(); - expect(screen.queryByLabelText(/^Runtime:/)).not.toBeInTheDocument(); - }); - - it("has no effect for non-agent actors", () => { - renderAvatar(); - expect(screen.queryByLabelText(/^Runtime:/)).not.toBeInTheDocument(); - }); -}); diff --git a/packages/views/common/agent-runtime-badge.tsx b/packages/views/common/agent-runtime-badge.tsx deleted file mode 100644 index ee80778b3e..0000000000 --- a/packages/views/common/agent-runtime-badge.tsx +++ /dev/null @@ -1,72 +0,0 @@ -"use client"; - -import { useAgentRuntimeProvider } from "@multica/core/agents"; -import { providerDisplayName } from "@multica/core/runtimes"; -import { useCurrentWorkspace } from "@multica/core/paths"; -import { AVATAR_SIZE_PX, type AvatarSize } from "@multica/ui/lib/avatar-size"; -import { ProviderLogo } from "../runtimes/components/provider-logo"; - -/** - * Smallest avatar that can carry a provider mark. - * - * A badge gets roughly 40% of the avatar's diameter, and provider marks are - * detailed artwork (Claude's glyph, CodeBuddy's tile) rather than a flat shape - * like the presence dot — below ~12px they read as a smudge. 32px is the first - * tier that clears it. That rules the badge out of exactly the surfaces the - * status dot lives on: nearly every `showStatusDot` call site is a 20px picker - * row, where "can it take work right now" is the question and the provider - * isn't. - */ -export const RUNTIME_BADGE_MIN_AVATAR_PX = 32; - -/** - * Provider mark overlaid on the top-right of an agent avatar — the opposite - * end from the presence dot, so a surface can carry both without them - * touching. - * - * Derived from the agent's current runtime on every render, so switching - * runtimes moves the mark with it. Renders nothing when the provider can't be - * resolved (loading, archived agent, deleted runtime): a wrong mark is worse - * than none. - * - * Lives in its own module rather than beside `AgentStatusDot` because the - * agent profile card needs it, and `actor-avatar` already imports that card - * for its hover payload — importing back would close a cycle. - */ -export function AgentRuntimeBadge({ - agentId, - size, -}: { - agentId: string; - size?: AvatarSize; -}) { - const ws = useCurrentWorkspace(); - const provider = useAgentRuntimeProvider(ws?.id, agentId); - const px = size ? AVATAR_SIZE_PX[size] : 24; - if (!provider || px < RUNTIME_BADGE_MIN_AVATAR_PX) return null; - - // The disc is background-coloured so a multi-coloured mark stays legible - // over an emoji or photo avatar; the mark itself fills most of it. The - // hairline is what makes it read as a chip sitting ON the avatar rather than - // an icon floating beside it — several provider marks (Claude's asterisk, - // Codex's ring) are airy line art with no silhouette of their own, and - // without an edge the badge loses its anchor. Same treatment as ProviderChip. - const disc = Math.round(px * 0.42); - // Push the badge out past the bounding box so its centre lands on the - // avatar's rim instead of inside it. Flush to the corner (`top-0 right-0`) - // looks correct on a square but not on a circle: at 45° the edge has already - // curved ~29% of the diameter away from that corner, so the badge ends up - // sitting over the face. Offsetting by (badge radius − rim inset) leaves - // roughly half of it overhanging, which is the usual platform-badge look. - const offset = Math.round(px * 0.064); - - return ( - - - - ); -}