| 1 | import type { |
| 2 | OrchestrationV2ThreadShell, |
| 3 | OrchestrationV2TurnItemStatus, |
| 4 | OrchestrationProjectShell, |
| 5 | ServerProvider, |
| 6 | ProviderDriverKind, |
| 7 | ProviderInstanceId, |
| 8 | OrchestrationV2Subagent, |
| 9 | } from "@t3tools/contracts"; |
| 10 | import { |
| 11 | resolveSubagentMetadata, |
| 12 | subagentDetailPreview, |
| 13 | } from "@t3tools/client-runtime/state/subagent-display"; |
| 14 | import { getModelSelectionStringOptionValue, resolveSelectableModel } from "@t3tools/shared/model"; |
| 15 | import { getTraitsSpeedDisplay, TraitsSpeedIcon } from "./TraitsSpeed"; |
| 16 | import type { ReactNode } from "react"; |
| 17 | import { |
| 18 | BotIcon, |
| 19 | CheckIcon, |
| 20 | CircleDashedIcon, |
| 21 | CircleXIcon, |
| 22 | FolderIcon, |
| 23 | GitBranchIcon, |
| 24 | TerminalIcon, |
| 25 | } from "lucide-react"; |
| 26 | import { ThreadHoverCard } from "../ThreadHoverCard"; |
| 27 | import { MiddleTruncate } from "../ui/middle-truncate"; |
| 28 | import { ProviderInstanceIcon } from "./ProviderInstanceIcon"; |
| 29 | import { cn } from "~/lib/utils"; |
| 30 | import { deriveProviderInstanceEntries, shouldShowInstanceBadge } from "../../providerInstances"; |
| 31 | |
| 32 | /** Geometry and preview limits stay identical in lineage and timeline tooltips. */ |
| 33 | export 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 | } |