Skip to content
Source
Sign inSign up
Open History to see the last commit

ProviderSubagentBar.tsx

78 lines3.4 KB

1import {
2 formatProviderSubagentStatus,
3 type ProviderSubagentStatus,
4} from "@t3tools/client-runtime/state/thread-execution";
5import { isOrchestrationV2WorkActive } from "@t3tools/contracts";
6import { ArrowUpLeftIcon } from "lucide-react";
7import type { ReactNode } from "react";
8​
9import { tickTextContent } from "~/lib/tickTextContent";
10import type { ProviderInstanceEntry } from "../../providerInstances";
11import { Button } from "../ui/button";
12import { ProviderInstanceIcon } from "./ProviderInstanceIcon";
13​
14export function ProviderSubagentBar(props: {
15 readonly provider: ProviderInstanceEntry | null;
16 readonly showInstanceBadge: boolean;
17 readonly modelLabel: string;
18 readonly effortLabel: string | null;
19 readonly status: ProviderSubagentStatus | null;
20 readonly onOpenParent: (() => void) | null;
21 readonly actions?: ReactNode;
22}) {
23 const { status } = props;
24 const live = status !== null && isOrchestrationV2WorkActive(status.status);
25 const modelDescription =
26 props.effortLabel === null ? props.modelLabel : `${props.modelLabel}, ${props.effortLabel}`;
27 const announcement = formatProviderSubagentStatus(
28 status === null ? null : { ...status, startedAt: null },
29 0,
30 );
31​
32 // Sized by the composer, not the viewport: a docked side panel narrows the
33 // chat column on a wide window, and one row there squeezes out the model
34 // and status, so the status drops to its own line instead.
35 return (
36 <div className="flex min-h-12 items-center gap-3 rounded-2xl py-2 ps-5 pe-2 text-sm">
37 <div className="flex min-w-0 flex-1 items-center gap-3 @max-[560px]/composer-surface:flex-col @max-[560px]/composer-surface:items-start @max-[560px]/composer-surface:gap-0.5">
38 <span className="flex min-w-0 max-w-full items-center gap-2">
39 {props.provider ? (
40 <ProviderInstanceIcon
41 driverKind={props.provider.driverKind}
42 displayName={props.provider.displayName}
43 accentColor={props.provider.accentColor}
44 acpRegistryAgentId={props.provider.acpRegistryAgentId}
45 acpRegistryIconUrl={props.provider.acpRegistryIconUrl}
46 showBadge={props.showInstanceBadge}
47 className="size-4 shrink-0"
48 iconClassName="size-4"
49 badgeClassName="right-[-0.125rem] bottom-[-0.125rem] h-3 min-w-3 px-0.5 text-5xs"
50 />
51 ) : null}
52 <span className="min-w-0 truncate font-medium text-foreground">{props.modelLabel}</span>
53 {props.effortLabel === null ? null : (
54 <span className="shrink-0 text-muted-foreground">{props.effortLabel}</span>
55 )}
56 </span>
57 <span
58 ref={tickTextContent((nowMs) => formatProviderSubagentStatus(status, nowMs), live)}
59 aria-hidden
60 className="min-w-0 max-w-full truncate text-muted-foreground tabular-nums @max-[560px]/composer-surface:text-xs"
61 />
62 </div>
63 <span role="status" className="sr-only">
64 {`${modelDescription} subagent: ${announcement}`}
65 </span>
66 <span className="shrink-0 text-muted-foreground @max-[640px]/composer-surface:sr-only">
67 Runs on its own
68 </span>
69 {props.actions}
70 {props.onOpenParent ? (
71 <Button size="sm" variant="ghost" onClick={props.onOpenParent}>
72 <ArrowUpLeftIcon />
73 <span className="@max-[320px]/composer-surface:sr-only">Open parent</span>
74 </Button>
75 ) : null}
76 </div>
77 );