Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 94 additions & 100 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -423,114 +424,107 @@ function SidebarThreadTooltip({
.map((instanceId) => providerEntryByInstanceId.get(instanceId)?.displayName ?? instanceId);
const supportsMultiplePullRequests = useSupportsMultiplePullRequests(thread.environmentId);
return (
<TooltipPopup
side="right"
align="start"
sideOffset={4}
variant="glass"
className="max-w-80 text-left whitespace-normal [&_[data-slot=tooltip-viewport]]:p-0"
>
<div className="flex min-w-0 max-w-80 flex-col gap-2 p-[var(--floating-content-inset)]">
<div className="min-w-0 truncate text-xs leading-tight font-medium text-foreground">
{thread.title}
</div>
<div className="grid gap-1.5 pl-0.5 text-xs text-muted-foreground">
{projectDisplayName ? (
<div className="flex min-w-0 items-center gap-2">
{project ? <ProjectFavicon project={project} className="size-3 shrink-0" /> : null}
<div className="min-w-0 truncate text-foreground/75">{projectDisplayName}</div>
</div>
) : null}
{environmentLabel ? (
<div className="flex min-w-0 items-center gap-2">
<EnvironmentMachineIcon
kind={environmentMachine}
className="size-3 shrink-0 stroke-muted-foreground"
/>
<div className="min-w-0 truncate text-foreground/75">{environmentLabel}</div>
</div>
) : null}
{thread.branch ? (
<div className="flex min-w-0 items-center gap-2">
<GitBranchIcon className="size-3 shrink-0 stroke-muted-foreground" />
<MiddleTruncate value={thread.branch} className="flex text-foreground/75" />
<ThreadHoverCardPopup side="right" align="start" sideOffset={4}>
<ThreadHoverCard
title={thread.title}
footer={
supportsMultiplePullRequests && thread.pullRequests.length > 0 ? (
<div className="border-t border-border/60 pt-2 pl-0.5 text-xs text-muted-foreground">
<ThreadPullRequestsMiniList pullRequests={thread.pullRequests} />
</div>
) : null}
{branchMismatch ? (
<div className="flex min-w-0 items-start gap-2 text-warning">
<CircleAlertIcon aria-hidden className="mt-0.5 size-3 shrink-0 stroke-current" />
<div className="min-w-0 flex-1 wrap-break-word leading-5">
You're currently checked out on another branch.
</div>
) : null
}
>
{projectDisplayName ? (
<div className="flex min-w-0 items-center gap-2">
{project ? <ProjectFavicon project={project} className="size-3 shrink-0" /> : null}
<div className="min-w-0 truncate text-foreground/75">{projectDisplayName}</div>
</div>
) : null}
{environmentLabel ? (
<div className="flex min-w-0 items-center gap-2">
<EnvironmentMachineIcon
kind={environmentMachine}
className="size-3 shrink-0 stroke-muted-foreground"
/>
<div className="min-w-0 truncate text-foreground/75">{environmentLabel}</div>
</div>
) : null}
{thread.branch ? (
<div className="flex min-w-0 items-center gap-2">
<GitBranchIcon className="size-3 shrink-0 stroke-muted-foreground" />
<MiddleTruncate value={thread.branch} className="flex text-foreground/75" />
</div>
) : null}
{branchMismatch ? (
<div className="flex min-w-0 items-start gap-2 text-warning">
<CircleAlertIcon aria-hidden className="mt-0.5 size-3 shrink-0 stroke-current" />
<div className="min-w-0 flex-1 wrap-break-word leading-5">
You're currently checked out on another branch.
</div>
) : null}
{driverKind ? (
<div className="flex min-w-0 items-center gap-2">
<ProviderInstanceIcon
driverKind={driverKind}
displayName={
providerEntry?.displayName ?? thread.runtime?.providerName ?? modelInstanceId
}
accentColor={providerEntry?.accentColor}
acpRegistryAgentId={providerEntry?.acpRegistryAgentId}
acpRegistryIconUrl={providerEntry?.acpRegistryIconUrl}
// Initials would swallow a size-3 glyph: accent dot, name in label.
showBadge={showInstanceBadge && providerEntry?.accentColor !== undefined}
badgeContent="none"
badgeClassName="h-2 min-w-2 px-0"
iconClassName="size-3 shrink-0 grayscale opacity-60"
/>
<div className="min-w-0 truncate text-foreground/75">
{showInstanceBadge && providerEntry
? `${modelLabel} · ${providerEntry.displayName}`
: modelLabel}
</div>
</div>
) : null}
{driverKind ? (
<div className="flex min-w-0 items-center gap-2">
<ProviderInstanceIcon
driverKind={driverKind}
displayName={
providerEntry?.displayName ?? thread.runtime?.providerName ?? modelInstanceId
}
accentColor={providerEntry?.accentColor}
acpRegistryAgentId={providerEntry?.acpRegistryAgentId}
acpRegistryIconUrl={providerEntry?.acpRegistryIconUrl}
// Initials would swallow a size-3 glyph: accent dot, name in label.
showBadge={showInstanceBadge && providerEntry?.accentColor !== undefined}
badgeContent="none"
badgeClassName="h-2 min-w-2 px-0"
iconClassName="size-3 shrink-0 grayscale opacity-60"
/>
<div className="min-w-0 truncate text-foreground/75">
{showInstanceBadge && providerEntry
? `${modelLabel} · ${providerEntry.displayName}`
: modelLabel}
</div>
) : null}
{previousProviderNames.length > 0 ? (
<div className="flex min-w-0 items-center gap-2">
<ArrowRightLeftIcon className="size-3 shrink-0 stroke-muted-foreground" />
<div className="min-w-0 truncate text-foreground/75">
Handed off from {previousProviderNames.join(", ")}
</div>
</div>
) : null}
{previousProviderNames.length > 0 ? (
<div className="flex min-w-0 items-center gap-2">
<ArrowRightLeftIcon className="size-3 shrink-0 stroke-muted-foreground" />
<div className="min-w-0 truncate text-foreground/75">
Handed off from {previousProviderNames.join(", ")}
</div>
) : null}
{terminalStatus ? (
<div className="flex min-w-0 items-center gap-2">
<TerminalIcon
aria-hidden
className={cn("size-3 shrink-0", terminalStatus.colorClass)}
/>
<div className="min-w-0 truncate text-foreground/75">
{terminalProcessLabel(terminalProcessCount)}
</div>
</div>
) : null}
{terminalStatus ? (
<div className="flex min-w-0 items-center gap-2">
<TerminalIcon
aria-hidden
className={cn("size-3 shrink-0", terminalStatus.colorClass)}
/>
<div className="min-w-0 truncate text-foreground/75">
{terminalProcessLabel(terminalProcessCount)}
</div>
) : null}
{thread.runtime?.lastError ? (
<div
className={cn(
"flex min-w-0 items-center gap-2",
thread.runtime.lastErrorClass === "usage_limit"
? "text-amber-700 dark:text-amber-300"
: "text-red-600 dark:text-red-400",
)}
>
<CircleAlertIcon className="size-3 shrink-0 stroke-current" />
<div className="min-w-0 truncate">
{thread.runtime.lastErrorClass === "usage_limit"
? "Usage limit reached"
: "Error occurred"}
</div>
</div>
) : null}
{thread.runtime?.lastError ? (
<div
className={cn(
"flex min-w-0 items-center gap-2",
thread.runtime.lastErrorClass === "usage_limit"
? "text-amber-700 dark:text-amber-300"
: "text-red-600 dark:text-red-400",
)}
>
<CircleAlertIcon className="size-3 shrink-0 stroke-current" />
<div className="min-w-0 truncate">
{thread.runtime.lastErrorClass === "usage_limit"
? "Usage limit reached"
: "Error occurred"}
</div>
) : null}
</div>
{supportsMultiplePullRequests && thread.pullRequests.length > 0 ? (
<div className="border-t border-border/60 pt-2 pl-0.5 text-xs text-muted-foreground">
<ThreadPullRequestsMiniList pullRequests={thread.pullRequests} />
</div>
) : null}
</div>
</TooltipPopup>
</ThreadHoverCard>
</ThreadHoverCardPopup>
);
}

Expand Down
37 changes: 37 additions & 0 deletions apps/web/src/components/ThreadHoverCard.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof TooltipPopup>) {
return (
<TooltipPopup
{...props}
variant="glass"
className={cn(
"max-w-80 text-left whitespace-normal [&_[data-slot=tooltip-viewport]]:p-0",
className,
)}
/>
);
}

/** Shared title, metadata spacing, and optional footer for thread previews. */
export function ThreadHoverCard({
title,
children,
footer,
}: {
title: string;
children: ReactNode;
footer?: ReactNode;
}) {
return (
<div className="flex min-w-0 max-w-80 flex-col gap-2 p-[var(--floating-content-inset)]">
<div className="min-w-0 truncate text-xs leading-tight font-medium text-foreground">
{title}
</div>
<div className="grid gap-1.5 pl-0.5 text-xs text-muted-foreground">{children}</div>
{footer}
</div>
);
}
5 changes: 4 additions & 1 deletion apps/web/src/components/chat/MessagesTimeline.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1914,7 +1914,10 @@ describe("MessagesTimeline", () => {
expect(child()).toHaveLength(0);
await act(() => group().props.onClick());
expect(child()).toHaveLength(1);
const content = JSON.stringify(renderer!.toJSON());
const content = renderer!.root
.findAll((node) => typeof node.type === "string")
.flatMap((node) => node.children.filter((child) => typeof child === "string"))
.join("");
expect(content).toContain(preview);
expect(content).not.toContain("Inspect the package");
if (result?.trim() && result !== preview) expect(content).not.toContain(result);
Expand Down
93 changes: 76 additions & 17 deletions apps/web/src/components/chat/SubagentTooltipContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<OrchestrationV2ThreadShell, "projectId" | "worktreePath"> | undefined;
childThread?:
| Pick<OrchestrationV2ThreadShell, "branch" | "worktreePath" | "modelSelection">
Expand Down Expand Up @@ -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", "waiting"].includes(props.status);
const failed = ["failed", "error"].includes(props.status);
const StatusIcon = working
? CircleDashedIcon
: failed
? CircleXIcon
: props.status === "completed"
? CheckIcon
: CircleDashedIcon;
return (
<div className="max-w-72 space-y-1 py-1">
<div className="truncate font-medium">{props.title}</div>
<dl className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1">
<dt className="text-muted-foreground">Model</dt>
<dd className="break-words text-right">{modelLabel}</dd>
<dt className="text-muted-foreground">Status</dt>
<dd className="text-right capitalize">{props.status.replaceAll("_", " ")}</dd>
{metadata.map(({ label, value }) => (
<Fragment key={label}>
<dt className="text-muted-foreground">{label}</dt>
<dd className="break-words text-right">{value}</dd>
</Fragment>
))}
</dl>
{preview ? <p className="line-clamp-3 break-words text-muted-foreground">{preview}</p> : null}
</div>
<ThreadHoverCard title={props.title}>
<div className="flex min-w-0 items-center gap-2">
{driver ? (
<ProviderInstanceIcon
driverKind={driver}
displayName={props.provider?.displayName ?? driver}
acpRegistryIconUrl={props.provider?.iconUrl}
iconClassName="size-3 shrink-0 grayscale opacity-60"
/>
) : (
<BotIcon className="size-3 shrink-0" />
)}
<span className="min-w-0 truncate text-foreground/75">{modelLabel}</span>
</div>
<div className="flex min-w-0 items-center justify-between gap-4">
<span
className={cn(
"inline-flex items-center gap-1 rounded-sm font-medium capitalize",
working
? "text-sky-600 dark:text-sky-400"
: failed
? "text-red-700 dark:text-red-300"
: props.status === "completed"
? "text-emerald-700 dark:text-emerald-300"
: "text-muted-foreground",
)}
>
<StatusIcon aria-hidden className="size-3 shrink-0" />
{props.status.replaceAll("_", " ")}
</span>
{props.elapsed}
</div>
{metadata.map(({ label, value }) => {
const Icon = label === "Branch" ? GitBranchIcon : FolderIcon;
return (
<div key={label} className="flex min-w-0 items-center gap-2">
<Icon aria-hidden className="size-3 shrink-0" />
<span className="sr-only">{label}</span>
<MiddleTruncate value={value} className="flex text-foreground/75" showTitle={false} />
</div>
);
})}
{preview ? (
<div className="flex min-w-0 items-center gap-2">
<TerminalIcon aria-hidden className="size-3 shrink-0" />
<MiddleTruncate value={preview} className="flex text-foreground/75" showTitle={false} />
</div>
) : null}
</ThreadHoverCard>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading
Loading