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 { useLayoutEffect, useRef } from "react";
8
9import type { ProviderInstanceEntry } from "../../providerInstances";
10import { Button } from "../ui/button";
11import { ProviderInstanceIcon } from "./ProviderInstanceIcon";
12
13/**
14 * Stands in for the composer on a provider-native subagent thread. The
15 * provider runs that conversation, so there is nothing to send; the bar says
16 * which model is working, for how long, and leads back to the parent.
17 */
18export function ProviderSubagentBar(props: {
19 /** The provider running the subagent; no icon while its catalog loads. */
20 readonly provider: ProviderInstanceEntry | null;
21 /** Several accounts on this provider: badge the glyph, as the composer does. */
22 readonly showInstanceBadge: boolean;
23 readonly modelLabel: string;
24 /** Reasoning effort as the composer names it, when the subagent has one. */
25 readonly effortLabel: string | null;
26 /** Null until the subagent's root turn arrives. */
27 readonly status: ProviderSubagentStatus | null;
28 readonly onOpenParent: (() => void) | null;
29}) {
30 const statusRef = useRef<HTMLSpanElement>(null);
31 const { status } = props;
32 const live = status !== null && isOrchestrationV2WorkActive(status.status);
33 const modelDescription =
34 props.effortLabel === null ? props.modelLabel : `${props.modelLabel}, ${props.effortLabel}`;
35 // Announced once per transition; the ticking label below is not.
36 const announcement = formatProviderSubagentStatus(
37 status === null ? null : { ...status, startedAt: null },
38 0,
39 );
40
41 // The label is written from an effect, and live bars tick through DOM
42 // writes, so a running timer never re-renders the chat view.
43 useLayoutEffect(() => {
44 const update = () => {
45 if (statusRef.current) {
46 statusRef.current.textContent = formatProviderSubagentStatus(status, Date.now());
47 }
48 };
49 update();
50 if (!live) return;
51 const id = setInterval(update, 1_000);
52 return () => clearInterval(id);
53 }, [live, status]);
54
55 return (
56 <div className="flex min-h-12 items-center gap-3 rounded-3xl py-2 ps-5 pe-2 text-sm">
57 <span className="flex min-w-0 items-center gap-2">
58 {props.provider ? (
59 <ProviderInstanceIcon
60 driverKind={props.provider.driverKind}
61 displayName={props.provider.displayName}
62 accentColor={props.provider.accentColor}
63 acpRegistryAgentId={props.provider.acpRegistryAgentId}
64 acpRegistryIconUrl={props.provider.acpRegistryIconUrl}
65 showBadge={props.showInstanceBadge}
66 className="size-4 shrink-0"
67 iconClassName="size-4"
68 badgeClassName="right-[-0.125rem] bottom-[-0.125rem] h-3 min-w-3 px-0.5 text-5xs"
69 />
70 ) : null}
71 <span className="min-w-0 truncate font-medium text-foreground">{props.modelLabel}</span>
72 {props.effortLabel === null ? null : (
73 <span className="shrink-0 text-muted-foreground">{props.effortLabel}</span>
74 )}
75 </span>
76 <span
77 ref={statusRef}
78 aria-hidden
79 className="min-w-0 truncate text-muted-foreground tabular-nums"
80 />
81 <span role="status" className="sr-only">
82 {`${modelDescription} subagent: ${announcement}`}
83 </span>
84 <span className="ms-auto shrink-0 text-muted-foreground max-sm:hidden">Runs on its own</span>
85 {props.onOpenParent ? (
86 <Button size="sm" variant="ghost" onClick={props.onOpenParent}>
87 <ArrowUpLeftIcon />
88 Open parent
89 </Button>
90 ) : null}
91 </div>
92 );
93}