Skip to content

Commit a5d3532

Browse files
fix(web): show provider account accent badge in sidebar rows and hover card (#5980)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent efe1773 commit a5d3532

4 files changed

Lines changed: 71 additions & 28 deletions

File tree

apps/web/src/components/Sidebar.tsx

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,11 @@ import {
158158
import { ProjectFavicon } from "./ProjectFavicon";
159159
import { ProviderInstanceIcon } from "./chat/ProviderInstanceIcon";
160160
import { getTriggerDisplayModelLabel } from "./chat/providerIconUtils";
161-
import { deriveProviderInstanceEntries, type ProviderInstanceEntry } from "../providerInstances";
161+
import {
162+
deriveProviderInstanceEntries,
163+
shouldShowInstanceBadge,
164+
type ProviderInstanceEntry,
165+
} from "../providerInstances";
162166
import { primaryServerProvidersAtom } from "../state/server";
163167
import { useThreadRunningTerminalIds } from "../state/terminalSessions";
164168
import { stackedThreadToast, toastManager } from "./ui/toast";
@@ -247,7 +251,8 @@ function SidebarThreadTooltip({
247251
projectCwd,
248252
projectFaviconPath,
249253
environmentLabel,
250-
driverKind,
254+
providerEntry,
255+
showInstanceBadge,
251256
modelInstanceId,
252257
modelLabel,
253258
branchMismatch,
@@ -259,7 +264,8 @@ function SidebarThreadTooltip({
259264
projectCwd: string | null;
260265
projectFaviconPath: string | null;
261266
environmentLabel: string | null;
262-
driverKind: ProviderInstanceEntry["driverKind"] | null;
267+
providerEntry: ProviderInstanceEntry | null;
268+
showInstanceBadge: boolean;
263269
modelInstanceId: string;
264270
modelLabel: string;
265271
branchMismatch: {
@@ -269,6 +275,7 @@ function SidebarThreadTooltip({
269275
terminalStatus: TerminalStatusIndicator | null;
270276
terminalProcessCount: number;
271277
}) {
278+
const driverKind = providerEntry?.driverKind ?? null;
272279
return (
273280
<TooltipPopup
274281
side="right"
@@ -317,10 +324,21 @@ function SidebarThreadTooltip({
317324
<div className="flex min-w-0 items-center gap-2">
318325
<ProviderInstanceIcon
319326
driverKind={driverKind}
320-
displayName={thread.session?.providerName ?? modelInstanceId}
327+
displayName={
328+
providerEntry?.displayName ?? thread.session?.providerName ?? modelInstanceId
329+
}
330+
accentColor={providerEntry?.accentColor}
331+
// Initials would swallow a size-3 glyph: accent dot, name in label.
332+
showBadge={showInstanceBadge && providerEntry?.accentColor !== undefined}
333+
badgeContent="none"
334+
badgeClassName="h-2 min-w-2 px-0"
321335
iconClassName="size-3 shrink-0 grayscale opacity-60"
322336
/>
323-
<div className="min-w-0 truncate text-foreground/75">{modelLabel}</div>
337+
<div className="min-w-0 truncate text-foreground/75">
338+
{showInstanceBadge && providerEntry
339+
? `${modelLabel} · ${providerEntry.displayName}`
340+
: modelLabel}
341+
</div>
324342
</div>
325343
) : null}
326344
{terminalStatus ? (
@@ -867,6 +885,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
867885
const modelInstanceId = thread.session?.providerInstanceId ?? thread.modelSelection.instanceId;
868886
const providerEntry = props.providerEntryByInstanceId.get(modelInstanceId) ?? null;
869887
const driverKind = providerEntry?.driverKind ?? null;
888+
const showInstanceBadge =
889+
providerEntry !== null &&
890+
shouldShowInstanceBadge(providerEntry, props.providerEntryByInstanceId.values());
870891
const selectedModel = providerEntry?.models.find(
871892
(model) => model.slug === thread.modelSelection.model,
872893
);
@@ -884,7 +905,8 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
884905
projectCwd={props.projectCwd}
885906
projectFaviconPath={props.projectFaviconPath}
886907
environmentLabel={props.environmentLabel}
887-
driverKind={driverKind}
908+
providerEntry={providerEntry}
909+
showInstanceBadge={showInstanceBadge}
888910
modelInstanceId={modelInstanceId}
889911
modelLabel={modelLabel}
890912
branchMismatch={branchMismatch}
@@ -1481,11 +1503,19 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
14811503
</span>
14821504
) : null}
14831505
{driverKind ? (
1484-
<span className="inline-flex shrink-0 items-center opacity-60">
1506+
<span className="inline-flex shrink-0 items-center">
14851507
<ProviderInstanceIcon
14861508
driverKind={driverKind}
1487-
displayName={thread.session?.providerName ?? modelInstanceId}
1488-
iconClassName="size-3.5"
1509+
displayName={
1510+
providerEntry?.displayName ??
1511+
thread.session?.providerName ??
1512+
modelInstanceId
1513+
}
1514+
accentColor={providerEntry?.accentColor}
1515+
showBadge={showInstanceBadge}
1516+
// Glyph dims, badge stays saturated; offset matches the composer trigger.
1517+
iconClassName="size-3.5 opacity-60"
1518+
badgeClassName="right-[-0.1875rem] bottom-[-0.1875rem] h-3 min-w-3 px-0.5 text-[7px]"
14891519
/>
14901520
</span>
14911521
) : null}
@@ -1542,7 +1572,9 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {
15421572
});
15431573
const modelInstanceId = thread.session?.providerInstanceId ?? thread.modelSelection.instanceId;
15441574
const providerEntry = props.providerEntryByInstanceId.get(modelInstanceId) ?? null;
1545-
const driverKind = providerEntry?.driverKind ?? null;
1575+
const showInstanceBadge =
1576+
providerEntry !== null &&
1577+
shouldShowInstanceBadge(providerEntry, props.providerEntryByInstanceId.values());
15461578
const selectedModel = providerEntry?.models.find(
15471579
(model) => model.slug === thread.modelSelection.model,
15481580
);
@@ -1600,7 +1632,8 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {
16001632
projectCwd={props.projectCwd}
16011633
projectFaviconPath={props.projectFaviconPath}
16021634
environmentLabel={props.environmentLabel}
1603-
driverKind={driverKind}
1635+
providerEntry={providerEntry}
1636+
showInstanceBadge={showInstanceBadge}
16041637
modelInstanceId={modelInstanceId}
16051638
modelLabel={modelLabel}
16061639
branchMismatch={branchMismatch}

apps/web/src/components/chat/ModelPickerSidebar.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { type ProviderInstanceId } from "@t3tools/contracts";
2-
import { memo, useLayoutEffect, useMemo, useRef, useState } from "react";
2+
import { memo, useLayoutEffect, useRef, useState } from "react";
33
import { SparklesIcon, StarIcon } from "lucide-react";
44
import { ProviderInstanceIcon } from "./ProviderInstanceIcon";
55
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
66
import { cn } from "~/lib/utils";
7-
import { isProviderInstancePickerReady, type ProviderInstanceEntry } from "../../providerInstances";
7+
import {
8+
isProviderInstancePickerReady,
9+
shouldShowInstanceBadge,
10+
type ProviderInstanceEntry,
11+
} from "../../providerInstances";
812

913
/**
1014
* Build the hover tooltip for an instance button. Mirrors the old
@@ -65,14 +69,6 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
6569
const [hoveredInstanceId, setHoveredInstanceId] = useState<ProviderInstanceId | null>(null);
6670
const sidebarContentRef = useRef<HTMLDivElement>(null);
6771
const [selectedIndicatorTop, setSelectedIndicatorTop] = useState<number | null>(null);
68-
const duplicateDriverCounts = useMemo(() => {
69-
const counts = new Map<string, number>();
70-
for (const entry of props.instanceEntries) {
71-
counts.set(entry.driverKind, (counts.get(entry.driverKind) ?? 0) + 1);
72-
}
73-
return counts;
74-
}, [props.instanceEntries]);
75-
7672
useLayoutEffect(() => {
7773
const content = sidebarContentRef.current;
7874
if (!content) {
@@ -143,8 +139,7 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
143139
const isSelected = props.selectedInstanceId === entry.instanceId;
144140
const isHovered = hoveredInstanceId === entry.instanceId;
145141
const showNewBadge = props.newBadgeInstanceIds?.has(entry.instanceId) ?? false;
146-
const showInstanceBadge =
147-
Boolean(entry.accentColor) || (duplicateDriverCounts.get(entry.driverKind) ?? 0) > 1;
142+
const showInstanceBadge = shouldShowInstanceBadge(entry, props.instanceEntries);
148143

149144
const tooltip = isUnavailable
150145
? describeUnavailableInstance(entry)

apps/web/src/components/chat/ProviderModelPicker.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
getTriggerDisplayModelLabel,
1717
getTriggerDisplayModelName,
1818
} from "./providerIconUtils";
19-
import type { ProviderInstanceEntry } from "../../providerInstances";
19+
import { shouldShowInstanceBadge, type ProviderInstanceEntry } from "../../providerInstances";
2020
import { ComposerControl, ComposerControlChevron } from "./ComposerControl";
2121

2222
export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
@@ -67,10 +67,8 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
6767
selectedInstanceOptions[0];
6868
const triggerTitle = selectedModel ? getTriggerDisplayModelName(selectedModel) : props.model;
6969
const triggerLabel = selectedModel ? getTriggerDisplayModelLabel(selectedModel) : props.model;
70-
const duplicateDriverCount = props.instanceEntries.filter(
71-
(entry) => activeEntry !== null && entry.driverKind === activeEntry.driverKind,
72-
).length;
73-
const showInstanceBadge = Boolean(activeEntry?.accentColor) || duplicateDriverCount > 1;
70+
const showInstanceBadge =
71+
activeEntry !== null && shouldShowInstanceBadge(activeEntry, props.instanceEntries);
7472

7573
const setIsMenuOpen = (open: boolean) => {
7674
props.onOpenChange?.(open);

apps/web/src/providerInstances.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,23 @@ function driverKindLabel(driverKind: ProviderDriverKind): string {
109109
return PROVIDER_DISPLAY_NAMES[driverKind] ?? formatProviderDriverKindLabel(driverKind);
110110
}
111111

112+
/**
113+
* Whether an instance's icon carries the account badge: accent color set, or
114+
* several instances sharing a driver so the brand glyph alone is ambiguous.
115+
* Shared by the composer trigger, the picker rail, and sidebar rows.
116+
*/
117+
export function shouldShowInstanceBadge(
118+
entry: ProviderInstanceEntry,
119+
entries: Iterable<ProviderInstanceEntry>,
120+
): boolean {
121+
if (entry.accentColor) return true;
122+
let sharedDriverCount = 0;
123+
for (const candidate of entries) {
124+
if (candidate.driverKind === entry.driverKind && ++sharedDriverCount > 1) return true;
125+
}
126+
return false;
127+
}
128+
112129
export function normalizeProviderAccentColor(value: string | undefined): string | undefined {
113130
const trimmed = value?.trim();
114131
if (!trimmed) return undefined;

0 commit comments

Comments
 (0)