From e7611ec317bbd71498c35b5dc13a21fef9606443 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 21 Sep 2026 03:38:43 +0000 Subject: [PATCH 1/3] feat(web): give lineage subagent hovers the thread hover card --- apps/web/src/components/Sidebar.tsx | 194 +++++++++--------- apps/web/src/components/ThreadHoverCard.tsx | 37 ++++ .../chat/SubagentTooltipContent.tsx | 93 +++++++-- ...ThreadRelationshipsControl.agents.test.tsx | 2 +- .../chat/ThreadRelationshipsControl.tsx | 8 +- .../src/components/chat/V2LifecycleRow.tsx | 13 +- 6 files changed, 224 insertions(+), 123 deletions(-) create mode 100644 apps/web/src/components/ThreadHoverCard.tsx diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index f24b2a36b017..3ea0d8a9c6b8 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -1,3 +1,4 @@ +import { ThreadHoverCard, ThreadHoverCardPopup } from "./ThreadHoverCard"; import { CollapsibleSectionHeader } from "./ui/collapsible-section-header"; import { setThreadChangeRequestSnapshot } from "./ThreadStatusIndicators"; import { ThreadContextDragGhost } from "./chat/ThreadContextDragGhost"; @@ -423,114 +424,107 @@ function SidebarThreadTooltip({ .map((instanceId) => providerEntryByInstanceId.get(instanceId)?.displayName ?? instanceId); const supportsMultiplePullRequests = useSupportsMultiplePullRequests(thread.environmentId); return ( - -
-
- {thread.title} -
-
- {projectDisplayName ? ( -
- {project ? : null} -
{projectDisplayName}
-
- ) : null} - {environmentLabel ? ( -
- -
{environmentLabel}
-
- ) : null} - {thread.branch ? ( -
- - + + 0 ? ( +
+
- ) : null} - {branchMismatch ? ( -
- -
- You're currently checked out on another branch. -
+ ) : null + } + > + {projectDisplayName ? ( +
+ {project ? : null} +
{projectDisplayName}
+
+ ) : null} + {environmentLabel ? ( +
+ +
{environmentLabel}
+
+ ) : null} + {thread.branch ? ( +
+ + +
+ ) : null} + {branchMismatch ? ( +
+ +
+ You're currently checked out on another branch.
- ) : null} - {driverKind ? ( -
- -
- {showInstanceBadge && providerEntry - ? `${modelLabel} · ${providerEntry.displayName}` - : modelLabel} -
+
+ ) : null} + {driverKind ? ( +
+ +
+ {showInstanceBadge && providerEntry + ? `${modelLabel} · ${providerEntry.displayName}` + : modelLabel}
- ) : null} - {previousProviderNames.length > 0 ? ( -
- -
- Handed off from {previousProviderNames.join(", ")} -
+
+ ) : null} + {previousProviderNames.length > 0 ? ( +
+ +
+ Handed off from {previousProviderNames.join(", ")}
- ) : null} - {terminalStatus ? ( -
- -
- {terminalProcessLabel(terminalProcessCount)} -
+
+ ) : null} + {terminalStatus ? ( +
+ +
+ {terminalProcessLabel(terminalProcessCount)}
- ) : null} - {thread.runtime?.lastError ? ( -
- -
- {thread.runtime.lastErrorClass === "usage_limit" - ? "Usage limit reached" - : "Error occurred"} -
+
+ ) : null} + {thread.runtime?.lastError ? ( +
+ +
+ {thread.runtime.lastErrorClass === "usage_limit" + ? "Usage limit reached" + : "Error occurred"}
- ) : null} -
- {supportsMultiplePullRequests && thread.pullRequests.length > 0 ? ( -
-
) : null} -
- + + ); } diff --git a/apps/web/src/components/ThreadHoverCard.tsx b/apps/web/src/components/ThreadHoverCard.tsx new file mode 100644 index 000000000000..03faf0d9481b --- /dev/null +++ b/apps/web/src/components/ThreadHoverCard.tsx @@ -0,0 +1,37 @@ +import type { ComponentProps, ReactNode } from "react"; +import { TooltipPopup } from "./ui/tooltip"; +import { cn } from "~/lib/utils"; + +export function ThreadHoverCardPopup({ className, ...props }: ComponentProps) { + return ( + + ); +} + +/** Shared title, metadata spacing, and optional footer for thread previews. */ +export function ThreadHoverCard({ + title, + children, + footer, +}: { + title: string; + children: ReactNode; + footer?: ReactNode; +}) { + return ( +
+
+ {title} +
+
{children}
+ {footer} +
+ ); +} diff --git a/apps/web/src/components/chat/SubagentTooltipContent.tsx b/apps/web/src/components/chat/SubagentTooltipContent.tsx index 8900f0f715d1..ff942f28bd60 100644 --- a/apps/web/src/components/chat/SubagentTooltipContent.tsx +++ b/apps/web/src/components/chat/SubagentTooltipContent.tsx @@ -2,17 +2,33 @@ import type { OrchestrationV2ThreadShell, OrchestrationProjectShell, ServerProvider, + ProviderDriverKind, } from "@t3tools/contracts"; import { fileBasename } from "@t3tools/client-runtime/markdown-links"; import { formatModelSlugName, resolveSelectableModel } from "@t3tools/shared/model"; import { getTriggerDisplayModelName } from "./providerIconUtils"; -import { Fragment } from "react"; +import type { ReactNode } from "react"; +import { + BotIcon, + CheckIcon, + CircleDashedIcon, + CircleXIcon, + FolderIcon, + GitBranchIcon, + TerminalIcon, +} from "lucide-react"; +import { ThreadHoverCard } from "../ThreadHoverCard"; +import { MiddleTruncate } from "../ui/middle-truncate"; +import { ProviderInstanceIcon } from "./ProviderInstanceIcon"; +import { cn } from "~/lib/utils"; /** Geometry and preview limits stay identical in lineage and timeline tooltips. */ export function SubagentTooltipContent(props: { title: string; model: string | null; provider?: ServerProvider | undefined; + driver?: ProviderDriverKind | undefined; + elapsed?: ReactNode; parentThread?: Pick | undefined; childThread?: | Pick @@ -61,22 +77,65 @@ export function SubagentTooltipContent(props: { const compactDetail = detail.trim().replace(/\s+/g, " "); const preview = compactDetail.length > 280 ? `${compactDetail.slice(0, 280).trimEnd()}…` : compactDetail; + const driver = props.provider?.driver ?? props.driver; + const working = ["running", "in_progress", "pending"].includes(props.status); + const failed = ["failed", "error"].includes(props.status); + const StatusIcon = working + ? CircleDashedIcon + : failed + ? CircleXIcon + : props.status === "completed" + ? CheckIcon + : CircleDashedIcon; return ( -
-
{props.title}
-
-
Model
-
{modelLabel}
-
Status
-
{props.status.replaceAll("_", " ")}
- {metadata.map(({ label, value }) => ( - -
{label}
-
{value}
-
- ))} -
- {preview ?

{preview}

: null} -
+ +
+ {driver ? ( + + ) : ( + + )} + {modelLabel} +
+
+ + + {props.status.replaceAll("_", " ")} + + {props.elapsed} +
+ {metadata.map(({ label, value }) => { + const Icon = label === "Branch" ? GitBranchIcon : FolderIcon; + return ( +
+ + {label} + +
+ ); + })} + {preview ? ( +
+ + +
+ ) : null} +
); } diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.agents.test.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.agents.test.tsx index 1e7dd8fc0436..c8ab5f40b4f5 100644 --- a/apps/web/src/components/chat/ThreadRelationshipsControl.agents.test.tsx +++ b/apps/web/src/components/chat/ThreadRelationshipsControl.agents.test.tsx @@ -257,7 +257,7 @@ it("shows readable models and only differing workspace details in agent tooltips renderer.root .findAll((node) => typeof node.type === "string") .flatMap((node) => node.children.filter((child) => typeof child === "string")) - .join(" "); + .join(""); expect(text()).toContain("My GPT"); expect(text()).not.toContain("Tokens"); expect(text()).not.toContain("Open subagent"); diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx index b07013bea588..3497eed8cab5 100644 --- a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx +++ b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx @@ -1,3 +1,4 @@ +import { ThreadHoverCardPopup } from "../ThreadHoverCard"; import { ThreadDetailsSection } from "./ThreadDetailsSection"; import { CollapsibleSectionHeader, SectionHeaderStatus } from "../ui/collapsible-section-header"; import { SubagentTooltipContent } from "./SubagentTooltipContent"; @@ -345,11 +346,14 @@ export function ThreadRelationshipsPanel(props: { const relationshipHint = node?.missing ? "This related thread is unavailable" : `Open ${relationship.toLowerCase()} in this chat`; + const RelationshipPopup = agent ? ThreadHoverCardPopup : TooltipPopup; const relationshipTooltip = agent ? ( } status={agent.status} result={agent.result} progress={agent.progress} @@ -405,7 +409,7 @@ export function ThreadRelationshipsPanel(props: { > {relationshipContent} - {relationshipTooltip} + {relationshipTooltip}