1import type {
2 OrchestrationV2ThreadShell,
3 OrchestrationV2TurnItemStatus,
4 OrchestrationProjectShell,
5 ServerProvider,
6 ProviderDriverKind,
7 ProviderInstanceId,
8 OrchestrationV2Subagent,
9} from "@t3tools/contracts";
10import {
11 resolveSubagentMetadata,
12 subagentDetailPreview,
13} from "@t3tools/client-runtime/state/subagent-display";
14import { getModelSelectionStringOptionValue, resolveSelectableModel } from "@t3tools/shared/model";
15import { getTraitsSpeedDisplay, TraitsSpeedIcon } from "./TraitsSpeed";
16import type { ReactNode } from "react";
17import {
18 BotIcon,
19 CheckIcon,
20 CircleDashedIcon,
21 CircleXIcon,
22 FolderIcon,
23 GitBranchIcon,
24 TerminalIcon,
25} from "lucide-react";
26import { ThreadHoverCard } from "../ThreadHoverCard";
27import { MiddleTruncate } from "../ui/middle-truncate";
28import { ProviderInstanceIcon } from "./ProviderInstanceIcon";
29import { cn } from "~/lib/utils";
30import { deriveProviderInstanceEntries, shouldShowInstanceBadge } from "../../providerInstances";
31
32/** Geometry and preview limits stay identical in lineage and timeline tooltips. */
33export function SubagentTooltipContent(props: {
34 title: string;
35 model: string | null;
36 providerInstanceId: ProviderInstanceId;
37 origin: OrchestrationV2Subagent["origin"];
38 modelSelection?: OrchestrationV2Subagent["modelSelection"];
39 provider?: ServerProvider | undefined;
40 /** The environment's instances; with several accounts on one provider, the card names this one. */
41 providers?: ReadonlyArray<ServerProvider> | undefined;
42 driver?: ProviderDriverKind | undefined;
43 elapsed?: ReactNode;
44 parentThread?: Pick<OrchestrationV2ThreadShell, "projectId" | "worktreePath"> | undefined;
45 childThread?:
46 | Pick<OrchestrationV2ThreadShell, "branch" | "worktreePath" | "modelSelection">
47 | undefined;
48 parentProject?: Pick<OrchestrationProjectShell, "workspaceRoot"> | undefined;
49 childProject?: Pick<OrchestrationProjectShell, "id" | "title" | "workspaceRoot"> | undefined;
50 status: OrchestrationV2TurnItemStatus;
51 result?: string | null | undefined;
52 progress?: string | null | undefined;
53}) {
54 const { modelLabel, workspace: metadata } = resolveSubagentMetadata(props);
55 const preview = subagentDetailPreview(props);
56 const model = props.model?.trim();
57 const modelSlug = props.provider
58 ? resolveSelectableModel(props.provider.driver, model, props.provider.models)
59 : model;
60 const providerModel = props.provider?.models.find((candidate) => candidate.slug === modelSlug);
61 const childSelection =
62 props.modelSelection ??
63 (props.origin === "app_owned" ? props.childThread?.modelSelection : undefined);
64 const childModel = props.provider
65 ? (resolveSelectableModel(
66 props.provider.driver,
67 childSelection?.model,
68 props.provider.models,
69 ) ?? childSelection?.model.trim())
70 : childSelection?.model.trim();
71 const provider = props.provider;
72 const matchingSelection =
73 childModel === (modelSlug ?? model) && childSelection?.instanceId === props.providerInstanceId
74 ? childSelection
75 : undefined;
76 const effort = ["reasoningEffort", "effort", "reasoning", "variant"]
77 .map((id) => getModelSelectionStringOptionValue(matchingSelection, id))
78 .find(Boolean);
79 const speed = provider
80 ? providerModel?.capabilities?.optionDescriptors
81 ?.map((descriptor) => {
82 const saved = matchingSelection?.options?.find((option) => option.id === descriptor.id);
83 if (
84 descriptor.id === "fastMode" &&
85 descriptor.type === "boolean" &&
86 typeof saved?.value === "boolean"
87 ) {
88 return getTraitsSpeedDisplay(provider.driver, {
89 ...descriptor,
90 currentValue: saved.value,
91 });
92 }
93 if (
94 provider.driver === "codex" &&
95 descriptor.id === "serviceTier" &&
96 descriptor.type === "select" &&
97 typeof saved?.value === "string" &&
98 descriptor.options.some((option) => option.id === saved.value)
99 ) {
100 return getTraitsSpeedDisplay(provider.driver, {
101 ...descriptor,
102 currentValue: saved.value,
103 });
104 }
105 return null;
106 })
107 .find(Boolean)
108 : undefined;
109 const driver = props.provider?.driver ?? props.driver;
110 const entries = deriveProviderInstanceEntries(props.providers ?? []);
111 const entry = entries.find((candidate) => candidate.instanceId === props.provider?.instanceId);
112 const showInstanceBadge = entry !== undefined && shouldShowInstanceBadge(entry, entries);
113 const working = ["running", "in_progress", "pending", "waiting"].includes(props.status);
114 const failed = ["failed", "error"].includes(props.status);
115 const StatusIcon = working
116 ? CircleDashedIcon
117 : failed
118 ? CircleXIcon
119 : props.status === "completed"
120 ? CheckIcon
121 : CircleDashedIcon;
122 return (
123 <ThreadHoverCard title={props.title}>
124 <div className="flex min-w-0 items-center gap-2">
125 {driver ? (
126 <ProviderInstanceIcon
127 driverKind={driver}
128 displayName={entry?.displayName ?? props.provider?.displayName ?? driver}
129 accentColor={entry?.accentColor}
130 acpRegistryIconUrl={props.provider?.iconUrl}
131 // Same treatment as the sidebar card: accent dot, account in the label.
132 showBadge={showInstanceBadge && entry?.accentColor !== undefined}
133 badgeContent="none"
134 badgeClassName="h-2 min-w-2 px-0"
135 iconClassName="size-3 shrink-0 grayscale opacity-60"
136 />
137 ) : (
138 <BotIcon className="size-3 shrink-0" />
139 )}
140 <span className="inline-flex min-w-0 items-center gap-1 text-foreground/75">
141 <span className="min-w-0 truncate">
142 {showInstanceBadge ? `${modelLabel} · ${entry.displayName}` : modelLabel}
143 </span>
144 {effort || speed?.speedIcon ? (
145 <span className="inline-flex shrink-0 items-center gap-1">
146 {effort ? " · " : null}
147 {speed?.speedIcon && provider ? (
148 <TraitsSpeedIcon provider={provider.driver} speedIcon={speed.speedIcon} size="xs" />
149 ) : null}
150 {effort}
151 </span>
152 ) : null}
153 </span>
154 </div>
155 <div className="flex min-w-0 items-center justify-between gap-4">
156 <span
157 className={cn(
158 "inline-flex items-center gap-1 rounded-sm font-medium capitalize",
159 working
160 ? "text-info"
161 : failed
162 ? "text-error"
163 : props.status === "completed"
164 ? "text-success"
165 : "text-muted-foreground",
166 )}
167 >
168 <StatusIcon aria-hidden className="size-3 shrink-0" />
169 {props.status.replaceAll("_", " ")}
170 </span>
171 {props.elapsed}
172 </div>
173 {metadata.map(({ label, value }) => {
174 const Icon = label === "Branch" ? GitBranchIcon : FolderIcon;
175 return (
176 <div key={label} className="flex min-w-0 items-center gap-2">
177 <Icon aria-hidden className="size-3 shrink-0" />
178 <span className="sr-only">{label}</span>
179 <MiddleTruncate value={value} className="flex" showTitle={false} />
180 </div>
181 );
182 })}
183 {preview ? (
184 <div className="flex min-w-0 items-center gap-2">
185 <TerminalIcon aria-hidden className="size-3 shrink-0" />
186 <MiddleTruncate value={preview} className="flex" showTitle={false} />
187 </div>
188 ) : null}
189 </ThreadHoverCard>
190 );
191}