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 {...@@ -158,7 +158,11 @@ import {
158import { ProjectFavicon } from "./ProjectFavicon";158import { ProjectFavicon } from "./ProjectFavicon";
159import { ProviderInstanceIcon } from "./chat/ProviderInstanceIcon";159import { ProviderInstanceIcon } from "./chat/ProviderInstanceIcon";
160import { getTriggerDisplayModelLabel } from "./chat/providerIconUtils";160import { getTriggerDisplayModelLabel } from "./chat/providerIconUtils";
161import { deriveProviderInstanceEntries, type ProviderInstanceEntry } from "../providerInstances";161import {
162 deriveProviderInstanceEntries,
163 shouldShowInstanceBadge,
164 type ProviderInstanceEntry,
165} from "../providerInstances";
162import { primaryServerProvidersAtom } from "../state/server";166import { primaryServerProvidersAtom } from "../state/server";
163import { useThreadRunningTerminalIds } from "../state/terminalSessions";167import { useThreadRunningTerminalIds } from "../state/terminalSessions";
164import { stackedThreadToast, toastManager } from "./ui/toast";168import { stackedThreadToast, toastManager } from "./ui/toast";
...@@ -247,7 +251,8 @@ function SidebarThreadTooltip({...@@ -247,7 +251,8 @@ function SidebarThreadTooltip({
247 projectCwd,251 projectCwd,
248 projectFaviconPath,252 projectFaviconPath,
249 environmentLabel,253 environmentLabel,
250 driverKind,254 providerEntry,
255 showInstanceBadge,
251 modelInstanceId,256 modelInstanceId,
252 modelLabel,257 modelLabel,
253 branchMismatch,258 branchMismatch,
...@@ -259,7 +264,8 @@ function SidebarThreadTooltip({...@@ -259,7 +264,8 @@ function SidebarThreadTooltip({
259 projectCwd: string | null;264 projectCwd: string | null;
260 projectFaviconPath: string | null;265 projectFaviconPath: string | null;
261 environmentLabel: string | null;266 environmentLabel: string | null;
262 driverKind: ProviderInstanceEntry["driverKind"] | null;267 providerEntry: ProviderInstanceEntry | null;
268 showInstanceBadge: boolean;
263 modelInstanceId: string;269 modelInstanceId: string;
264 modelLabel: string;270 modelLabel: string;
265 branchMismatch: {271 branchMismatch: {
...@@ -269,6 +275,7 @@ function SidebarThreadTooltip({...@@ -269,6 +275,7 @@ function SidebarThreadTooltip({
269 terminalStatus: TerminalStatusIndicator | null;275 terminalStatus: TerminalStatusIndicator | null;
270 terminalProcessCount: number;276 terminalProcessCount: number;
271}) {277}) {
278 const driverKind = providerEntry?.driverKind ?? null;
272 return (279 return (
273 <TooltipPopup280 <TooltipPopup
274 side="right"281 side="right"
...@@ -317,10 +324,21 @@ function SidebarThreadTooltip({...@@ -317,10 +324,21 @@ function SidebarThreadTooltip({
317 <div className="flex min-w-0 items-center gap-2">324 <div className="flex min-w-0 items-center gap-2">
318 <ProviderInstanceIcon325 <ProviderInstanceIcon
319 driverKind={driverKind}326 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"
321 iconClassName="size-3 shrink-0 grayscale opacity-60"335 iconClassName="size-3 shrink-0 grayscale opacity-60"
322 />336 />
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>
324 </div>342 </div>
325 ) : null}343 ) : null}
326 {terminalStatus ? (344 {terminalStatus ? (
...@@ -867,6 +885,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -867,6 +885,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
867 const modelInstanceId = thread.session?.providerInstanceId ?? thread.modelSelection.instanceId;885 const modelInstanceId = thread.session?.providerInstanceId ?? thread.modelSelection.instanceId;
868 const providerEntry = props.providerEntryByInstanceId.get(modelInstanceId) ?? null;886 const providerEntry = props.providerEntryByInstanceId.get(modelInstanceId) ?? null;
869 const driverKind = providerEntry?.driverKind ?? null;887 const driverKind = providerEntry?.driverKind ?? null;
888 const showInstanceBadge =
889 providerEntry !== null &&
890 shouldShowInstanceBadge(providerEntry, props.providerEntryByInstanceId.values());
870 const selectedModel = providerEntry?.models.find(891 const selectedModel = providerEntry?.models.find(
871 (model) => model.slug === thread.modelSelection.model,892 (model) => model.slug === thread.modelSelection.model,
872 );893 );
...@@ -884,7 +905,8 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -884,7 +905,8 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
884 projectCwd={props.projectCwd}905 projectCwd={props.projectCwd}
885 projectFaviconPath={props.projectFaviconPath}906 projectFaviconPath={props.projectFaviconPath}
886 environmentLabel={props.environmentLabel}907 environmentLabel={props.environmentLabel}
887 driverKind={driverKind}908 providerEntry={providerEntry}
909 showInstanceBadge={showInstanceBadge}
888 modelInstanceId={modelInstanceId}910 modelInstanceId={modelInstanceId}
889 modelLabel={modelLabel}911 modelLabel={modelLabel}
890 branchMismatch={branchMismatch}912 branchMismatch={branchMismatch}
...@@ -1481,11 +1503,19 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1481,11 +1503,19 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1481 </span>1503 </span>
1482 ) : null}1504 ) : null}
1483 {driverKind ? (1505 {driverKind ? (
1484 <span className="inline-flex shrink-0 items-center opacity-60">1506 <span className="inline-flex shrink-0 items-center">
1485 <ProviderInstanceIcon1507 <ProviderInstanceIcon
1486 driverKind={driverKind}1508 driverKind={driverKind}
1487 displayName={thread.session?.providerName ?? modelInstanceId}1509 displayName={
1488 iconClassName="size-3.5"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]"
1489 />1519 />
1490 </span>1520 </span>
1491 ) : null}1521 ) : null}
...@@ -1542,7 +1572,9 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {...@@ -1542,7 +1572,9 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {
1542 });1572 });
1543 const modelInstanceId = thread.session?.providerInstanceId ?? thread.modelSelection.instanceId;1573 const modelInstanceId = thread.session?.providerInstanceId ?? thread.modelSelection.instanceId;
1544 const providerEntry = props.providerEntryByInstanceId.get(modelInstanceId) ?? null;1574 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());
1546 const selectedModel = providerEntry?.models.find(1578 const selectedModel = providerEntry?.models.find(
1547 (model) => model.slug === thread.modelSelection.model,1579 (model) => model.slug === thread.modelSelection.model,
1548 );1580 );
...@@ -1600,7 +1632,8 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {...@@ -1600,7 +1632,8 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {
1600 projectCwd={props.projectCwd}1632 projectCwd={props.projectCwd}
1601 projectFaviconPath={props.projectFaviconPath}1633 projectFaviconPath={props.projectFaviconPath}
1602 environmentLabel={props.environmentLabel}1634 environmentLabel={props.environmentLabel}
1603 driverKind={driverKind}1635 providerEntry={providerEntry}
1636 showInstanceBadge={showInstanceBadge}
1604 modelInstanceId={modelInstanceId}1637 modelInstanceId={modelInstanceId}
1605 modelLabel={modelLabel}1638 modelLabel={modelLabel}
1606 branchMismatch={branchMismatch}1639 branchMismatch={branchMismatch}
apps/web/src/components/chat/ModelPickerSidebar.tsx+7-12
...@@ -1,10 +1,14 @@...@@ -1,10 +1,14 @@
1import { type ProviderInstanceId } from "@t3tools/contracts";1import { type ProviderInstanceId } from "@t3tools/contracts";
2import { memo, useLayoutEffect, useMemo, useRef, useState } from "react";2import { memo, useLayoutEffect, useRef, useState } from "react";
3import { SparklesIcon, StarIcon } from "lucide-react";3import { SparklesIcon, StarIcon } from "lucide-react";
4import { ProviderInstanceIcon } from "./ProviderInstanceIcon";4import { ProviderInstanceIcon } from "./ProviderInstanceIcon";
5import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";5import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
6import { cn } from "~/lib/utils";6import { cn } from "~/lib/utils";
7import { isProviderInstancePickerReady, type ProviderInstanceEntry } from "../../providerInstances";7import {
8 isProviderInstancePickerReady,
9 shouldShowInstanceBadge,
10 type ProviderInstanceEntry,
11} from "../../providerInstances";
812
9/**13/**
10 * Build the hover tooltip for an instance button. Mirrors the old14 * Build the hover tooltip for an instance button. Mirrors the old
...@@ -65,14 +69,6 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {...@@ -65,14 +69,6 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
65 const [hoveredInstanceId, setHoveredInstanceId] = useState<ProviderInstanceId | null>(null);69 const [hoveredInstanceId, setHoveredInstanceId] = useState<ProviderInstanceId | null>(null);
66 const sidebarContentRef = useRef<HTMLDivElement>(null);70 const sidebarContentRef = useRef<HTMLDivElement>(null);
67 const [selectedIndicatorTop, setSelectedIndicatorTop] = useState<number | null>(null);71 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
76 useLayoutEffect(() => {72 useLayoutEffect(() => {
77 const content = sidebarContentRef.current;73 const content = sidebarContentRef.current;
78 if (!content) {74 if (!content) {
...@@ -143,8 +139,7 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {...@@ -143,8 +139,7 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
143 const isSelected = props.selectedInstanceId === entry.instanceId;139 const isSelected = props.selectedInstanceId === entry.instanceId;
144 const isHovered = hoveredInstanceId === entry.instanceId;140 const isHovered = hoveredInstanceId === entry.instanceId;
145 const showNewBadge = props.newBadgeInstanceIds?.has(entry.instanceId) ?? false;141 const showNewBadge = props.newBadgeInstanceIds?.has(entry.instanceId) ?? false;
146 const showInstanceBadge =142 const showInstanceBadge = shouldShowInstanceBadge(entry, props.instanceEntries);
147 Boolean(entry.accentColor) || (duplicateDriverCounts.get(entry.driverKind) ?? 0) > 1;
148143
149 const tooltip = isUnavailable144 const tooltip = isUnavailable
150 ? describeUnavailableInstance(entry)145 ? describeUnavailableInstance(entry)
apps/web/src/components/chat/ProviderModelPicker.tsx+3-5
...@@ -16,7 +16,7 @@ import {...@@ -16,7 +16,7 @@ import {
16 getTriggerDisplayModelLabel,16 getTriggerDisplayModelLabel,
17 getTriggerDisplayModelName,17 getTriggerDisplayModelName,
18} from "./providerIconUtils";18} from "./providerIconUtils";
19import type { ProviderInstanceEntry } from "../../providerInstances";19import { shouldShowInstanceBadge, type ProviderInstanceEntry } from "../../providerInstances";
20import { ComposerControl, ComposerControlChevron } from "./ComposerControl";20import { ComposerControl, ComposerControlChevron } from "./ComposerControl";
2121
22export const ProviderModelPicker = memo(function ProviderModelPicker(props: {22export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
...@@ -67,10 +67,8 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {...@@ -67,10 +67,8 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
67 selectedInstanceOptions[0];67 selectedInstanceOptions[0];
68 const triggerTitle = selectedModel ? getTriggerDisplayModelName(selectedModel) : props.model;68 const triggerTitle = selectedModel ? getTriggerDisplayModelName(selectedModel) : props.model;
69 const triggerLabel = selectedModel ? getTriggerDisplayModelLabel(selectedModel) : props.model;69 const triggerLabel = selectedModel ? getTriggerDisplayModelLabel(selectedModel) : props.model;
70 const duplicateDriverCount = props.instanceEntries.filter(70 const showInstanceBadge =
71 (entry) => activeEntry !== null && entry.driverKind === activeEntry.driverKind,71 activeEntry !== null && shouldShowInstanceBadge(activeEntry, props.instanceEntries);
72 ).length;
73 const showInstanceBadge = Boolean(activeEntry?.accentColor) || duplicateDriverCount > 1;
7472
75 const setIsMenuOpen = (open: boolean) => {73 const setIsMenuOpen = (open: boolean) => {
76 props.onOpenChange?.(open);74 props.onOpenChange?.(open);
apps/web/src/providerInstances.ts+17
...@@ -109,6 +109,23 @@ function driverKindLabel(driverKind: ProviderDriverKind): string {...@@ -109,6 +109,23 @@ function driverKindLabel(driverKind: ProviderDriverKind): string {
109 return PROVIDER_DISPLAY_NAMES[driverKind] ?? formatProviderDriverKindLabel(driverKind);109 return PROVIDER_DISPLAY_NAMES[driverKind] ?? formatProviderDriverKindLabel(driverKind);
110}110}
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
112export function normalizeProviderAccentColor(value: string | undefined): string | undefined {129export function normalizeProviderAccentColor(value: string | undefined): string | undefined {
113 const trimmed = value?.trim();130 const trimmed = value?.trim();
114 if (!trimmed) return undefined;131 if (!trimmed) return undefined;