authorgravatar for vitalyiegorov@gmail.comVitaly Iegorov <vitalyiegorov@gmail.com> 2026-08-15 14:06:50+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-15 12:06:50+00:00
loga5d35321bd620400dc03ddf7a42ee2672b225ac3
tree0d2fc75eee8faa8a64a0b7d4ba68c0d3a4f4d035
parentefe1773e9c5222857b88c006d47215b023703a04
signature Signed by PGP key B5690EEEBB952194

fix(web): show provider account accent badge in sidebar rows and hover card (#5980)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

4 files changed, 71 insertions(+), 28 deletions(-)

apps/web/src/components/Sidebar.tsx+44-11
......@@ -158,7 +158,11 @@ import {
158158import { ProjectFavicon } from "./ProjectFavicon";
159159import { ProviderInstanceIcon } from "./chat/ProviderInstanceIcon";
160160import { getTriggerDisplayModelLabel } from "./chat/providerIconUtils";
161import { deriveProviderInstanceEntries, type ProviderInstanceEntry } from "../providerInstances";
161import {
162 deriveProviderInstanceEntries,
163 shouldShowInstanceBadge,
164 type ProviderInstanceEntry,
165} from "../providerInstances";
162166import { primaryServerProvidersAtom } from "../state/server";
163167import { useThreadRunningTerminalIds } from "../state/terminalSessions";
164168import { 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+7-12
......@@ -1,10 +1,14 @@
11import { type ProviderInstanceId } from "@t3tools/contracts";
2import { memo, useLayoutEffect, useMemo, useRef, useState } from "react";
2import { memo, useLayoutEffect, useRef, useState } from "react";
33import { SparklesIcon, StarIcon } from "lucide-react";
44import { ProviderInstanceIcon } from "./ProviderInstanceIcon";
55import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
66import { cn } from "~/lib/utils";
7import { isProviderInstancePickerReady, type ProviderInstanceEntry } from "../../providerInstances";
7import {
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+3-5
......@@ -16,7 +16,7 @@ import {
1616 getTriggerDisplayModelLabel,
1717 getTriggerDisplayModelName,
1818} from "./providerIconUtils";
19import type { ProviderInstanceEntry } from "../../providerInstances";
19import { shouldShowInstanceBadge, type ProviderInstanceEntry } from "../../providerInstances";
2020import { ComposerControl, ComposerControlChevron } from "./ComposerControl";
2121
2222export 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+17
......@@ -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 */
117export 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
112129export function normalizeProviderAccentColor(value: string | undefined): string | undefined {
113130 const trimmed = value?.trim();
114131 if (!trimmed) return undefined;