| 1 | import { |
| 2 | formatProviderSubagentStatus, |
| 3 | type ProviderSubagentStatus, |
| 4 | } from "@t3tools/client-runtime/state/thread-execution"; |
| 5 | import { isOrchestrationV2WorkActive } from "@t3tools/contracts"; |
| 6 | import { ArrowUpLeftIcon } from "lucide-react"; |
| 7 | import { useLayoutEffect, useRef } from "react"; |
| 8 | |
| 9 | import type { ProviderInstanceEntry } from "../../providerInstances"; |
| 10 | import { Button } from "../ui/button"; |
| 11 | import { 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 | */ |
| 18 | export 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 | } |