From 931e4d1de52ceacd3257171475116b2d7038626c Mon Sep 17 00:00:00 2001 From: Vitalii Yehorov Date: Mon, 10 Aug 2026 10:48:23 +0200 Subject: [PATCH 1/2] fix(web): show provider account accent badge in sidebar rows and hover card Sidebar v2 rows and their details tooltip rendered the provider glyph without the account accent color and initials badge the composer's model picker shows, so threads from different accounts of the same provider were indistinguishable. Thread the already-resolved ProviderInstanceEntry into the two ProviderInstanceIcon call sites and reuse the composer's badge heuristic (custom accent color set, or several accounts on one provider). The tooltip also names the account next to the model label. Fixes #5977 Co-Authored-By: Claude Fable 5 --- apps/web/src/components/Sidebar.tsx | 68 ++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 76d678cb35c..d61095e4573 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -238,13 +238,29 @@ function terminalProcessLabel(count: number): string { return `${count} terminal ${count === 1 ? "process" : "processes"} running`; } +// Several accounts on the same provider share one glyph, so the account badge +// is the only thing telling them apart. +function hasDuplicateDriverInstances( + entries: ReadonlyMap, + driverKind: ProviderInstanceEntry["driverKind"], +): boolean { + let seen = false; + for (const entry of entries.values()) { + if (entry.driverKind !== driverKind) continue; + if (seen) return true; + seen = true; + } + return false; +} + function SidebarThreadTooltip({ thread, projectTitle, projectCwd, projectFaviconPath, environmentLabel, - driverKind, + providerEntry, + showInstanceBadge, modelInstanceId, modelLabel, branchMismatch, @@ -256,7 +272,8 @@ function SidebarThreadTooltip({ projectCwd: string | null; projectFaviconPath: string | null; environmentLabel: string | null; - driverKind: ProviderInstanceEntry["driverKind"] | null; + providerEntry: ProviderInstanceEntry | null; + showInstanceBadge: boolean; modelInstanceId: string; modelLabel: string; branchMismatch: { @@ -266,6 +283,7 @@ function SidebarThreadTooltip({ terminalStatus: TerminalStatusIndicator | null; terminalProcessCount: number; }) { + const driverKind = providerEntry?.driverKind ?? null; return ( -
{modelLabel}
+
+ {showInstanceBadge && providerEntry + ? `${modelLabel} ยท ${providerEntry.displayName}` + : modelLabel} +
) : null} {terminalStatus ? ( @@ -858,6 +888,10 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { const modelInstanceId = thread.session?.providerInstanceId ?? thread.modelSelection.instanceId; const providerEntry = props.providerEntryByInstanceId.get(modelInstanceId) ?? null; const driverKind = providerEntry?.driverKind ?? null; + const showInstanceBadge = + providerEntry !== null && + (Boolean(providerEntry.accentColor) || + hasDuplicateDriverInstances(props.providerEntryByInstanceId, providerEntry.driverKind)); const selectedModel = providerEntry?.models.find( (model) => model.slug === thread.modelSelection.model, ); @@ -875,7 +909,8 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { projectCwd={props.projectCwd} projectFaviconPath={props.projectFaviconPath} environmentLabel={props.environmentLabel} - driverKind={driverKind} + providerEntry={providerEntry} + showInstanceBadge={showInstanceBadge} modelInstanceId={modelInstanceId} modelLabel={modelLabel} branchMismatch={branchMismatch} @@ -1453,11 +1488,20 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { ) : null} {driverKind ? ( - + ) : null} @@ -1514,7 +1558,10 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: { }); const modelInstanceId = thread.session?.providerInstanceId ?? thread.modelSelection.instanceId; const providerEntry = props.providerEntryByInstanceId.get(modelInstanceId) ?? null; - const driverKind = providerEntry?.driverKind ?? null; + const showInstanceBadge = + providerEntry !== null && + (Boolean(providerEntry.accentColor) || + hasDuplicateDriverInstances(props.providerEntryByInstanceId, providerEntry.driverKind)); const selectedModel = providerEntry?.models.find( (model) => model.slug === thread.modelSelection.model, ); @@ -1572,7 +1619,8 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: { projectCwd={props.projectCwd} projectFaviconPath={props.projectFaviconPath} environmentLabel={props.environmentLabel} - driverKind={driverKind} + providerEntry={providerEntry} + showInstanceBadge={showInstanceBadge} modelInstanceId={modelInstanceId} modelLabel={modelLabel} branchMismatch={branchMismatch} From 7e106393ea70b649ee01d2a66a09b9f3ecbb9ea0 Mon Sep 17 00:00:00 2001 From: Vitalii Yehorov Date: Mon, 10 Aug 2026 11:13:27 +0200 Subject: [PATCH 2/2] fix(web): offset sidebar badge off the glyph and share the badge heuristic The row badge sat fully inside the 14px glyph and swallowed it; use the same corner-offset geometry the composer trigger already uses. Extract the duplicated show-badge heuristic (three hand-rolled copies in the sidebar rows, ProviderModelPicker, and ModelPickerSidebar) into shouldShowInstanceBadge in providerInstances.ts. Co-Authored-By: Claude Fable 5 --- apps/web/src/components/Sidebar.tsx | 35 ++++++------------- .../components/chat/ModelPickerSidebar.tsx | 19 ++++------ .../components/chat/ProviderModelPicker.tsx | 8 ++--- apps/web/src/providerInstances.ts | 17 +++++++++ 4 files changed, 37 insertions(+), 42 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index d61095e4573..23aa8720c35 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -155,7 +155,11 @@ import { import { ProjectFavicon } from "./ProjectFavicon"; import { ProviderInstanceIcon } from "./chat/ProviderInstanceIcon"; import { getTriggerDisplayModelLabel } from "./chat/providerIconUtils"; -import { deriveProviderInstanceEntries, type ProviderInstanceEntry } from "../providerInstances"; +import { + deriveProviderInstanceEntries, + shouldShowInstanceBadge, + type ProviderInstanceEntry, +} from "../providerInstances"; import { primaryServerProvidersAtom } from "../state/server"; import { useThreadRunningTerminalIds } from "../state/terminalSessions"; import { stackedThreadToast, toastManager } from "./ui/toast"; @@ -238,21 +242,6 @@ function terminalProcessLabel(count: number): string { return `${count} terminal ${count === 1 ? "process" : "processes"} running`; } -// Several accounts on the same provider share one glyph, so the account badge -// is the only thing telling them apart. -function hasDuplicateDriverInstances( - entries: ReadonlyMap, - driverKind: ProviderInstanceEntry["driverKind"], -): boolean { - let seen = false; - for (const entry of entries.values()) { - if (entry.driverKind !== driverKind) continue; - if (seen) return true; - seen = true; - } - return false; -} - function SidebarThreadTooltip({ thread, projectTitle, @@ -336,8 +325,7 @@ function SidebarThreadTooltip({ providerEntry?.displayName ?? thread.session?.providerName ?? modelInstanceId } accentColor={providerEntry?.accentColor} - // Initials would swallow a size-3 glyph, so the accent reads as - // a plain dot here and the account name lands in the label. + // 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" @@ -890,8 +878,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { const driverKind = providerEntry?.driverKind ?? null; const showInstanceBadge = providerEntry !== null && - (Boolean(providerEntry.accentColor) || - hasDuplicateDriverInstances(props.providerEntryByInstanceId, providerEntry.driverKind)); + shouldShowInstanceBadge(providerEntry, props.providerEntryByInstanceId.values()); const selectedModel = providerEntry?.models.find( (model) => model.slug === thread.modelSelection.model, ); @@ -1498,10 +1485,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { } accentColor={providerEntry?.accentColor} showBadge={showInstanceBadge} - // Only the glyph dims; the accent badge has to stay - // saturated to read as an account color. + // Glyph dims, badge stays saturated; offset matches the composer trigger. iconClassName="size-3.5 opacity-60" - badgeClassName="h-3 min-w-3 px-0.5 text-[7px]" + badgeClassName="right-[-0.1875rem] bottom-[-0.1875rem] h-3 min-w-3 px-0.5 text-[7px]" /> ) : null} @@ -1560,8 +1546,7 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: { const providerEntry = props.providerEntryByInstanceId.get(modelInstanceId) ?? null; const showInstanceBadge = providerEntry !== null && - (Boolean(providerEntry.accentColor) || - hasDuplicateDriverInstances(props.providerEntryByInstanceId, providerEntry.driverKind)); + shouldShowInstanceBadge(providerEntry, props.providerEntryByInstanceId.values()); const selectedModel = providerEntry?.models.find( (model) => model.slug === thread.modelSelection.model, ); diff --git a/apps/web/src/components/chat/ModelPickerSidebar.tsx b/apps/web/src/components/chat/ModelPickerSidebar.tsx index 05b44dcb732..df35cbd90e5 100644 --- a/apps/web/src/components/chat/ModelPickerSidebar.tsx +++ b/apps/web/src/components/chat/ModelPickerSidebar.tsx @@ -1,10 +1,14 @@ import { type ProviderInstanceId } from "@t3tools/contracts"; -import { memo, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { memo, useLayoutEffect, useRef, useState } from "react"; import { SparklesIcon, StarIcon } from "lucide-react"; import { ProviderInstanceIcon } from "./ProviderInstanceIcon"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { cn } from "~/lib/utils"; -import { isProviderInstancePickerReady, type ProviderInstanceEntry } from "../../providerInstances"; +import { + isProviderInstancePickerReady, + shouldShowInstanceBadge, + type ProviderInstanceEntry, +} from "../../providerInstances"; /** * Build the hover tooltip for an instance button. Mirrors the old @@ -65,14 +69,6 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { const [hoveredInstanceId, setHoveredInstanceId] = useState(null); const sidebarContentRef = useRef(null); const [selectedIndicatorTop, setSelectedIndicatorTop] = useState(null); - const duplicateDriverCounts = useMemo(() => { - const counts = new Map(); - for (const entry of props.instanceEntries) { - counts.set(entry.driverKind, (counts.get(entry.driverKind) ?? 0) + 1); - } - return counts; - }, [props.instanceEntries]); - useLayoutEffect(() => { const content = sidebarContentRef.current; if (!content) { @@ -143,8 +139,7 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { const isSelected = props.selectedInstanceId === entry.instanceId; const isHovered = hoveredInstanceId === entry.instanceId; const showNewBadge = props.newBadgeInstanceIds?.has(entry.instanceId) ?? false; - const showInstanceBadge = - Boolean(entry.accentColor) || (duplicateDriverCounts.get(entry.driverKind) ?? 0) > 1; + const showInstanceBadge = shouldShowInstanceBadge(entry, props.instanceEntries); const tooltip = isUnavailable ? describeUnavailableInstance(entry) diff --git a/apps/web/src/components/chat/ProviderModelPicker.tsx b/apps/web/src/components/chat/ProviderModelPicker.tsx index a9b3a398115..bd374a0fd6f 100644 --- a/apps/web/src/components/chat/ProviderModelPicker.tsx +++ b/apps/web/src/components/chat/ProviderModelPicker.tsx @@ -16,7 +16,7 @@ import { getTriggerDisplayModelLabel, getTriggerDisplayModelName, } from "./providerIconUtils"; -import type { ProviderInstanceEntry } from "../../providerInstances"; +import { shouldShowInstanceBadge, type ProviderInstanceEntry } from "../../providerInstances"; import { ComposerControl, ComposerControlChevron } from "./ComposerControl"; export const ProviderModelPicker = memo(function ProviderModelPicker(props: { @@ -67,10 +67,8 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { selectedInstanceOptions[0]; const triggerTitle = selectedModel ? getTriggerDisplayModelName(selectedModel) : props.model; const triggerLabel = selectedModel ? getTriggerDisplayModelLabel(selectedModel) : props.model; - const duplicateDriverCount = props.instanceEntries.filter( - (entry) => activeEntry !== null && entry.driverKind === activeEntry.driverKind, - ).length; - const showInstanceBadge = Boolean(activeEntry?.accentColor) || duplicateDriverCount > 1; + const showInstanceBadge = + activeEntry !== null && shouldShowInstanceBadge(activeEntry, props.instanceEntries); const setIsMenuOpen = (open: boolean) => { props.onOpenChange?.(open); diff --git a/apps/web/src/providerInstances.ts b/apps/web/src/providerInstances.ts index 337e68d44d0..fd4ca7da92d 100644 --- a/apps/web/src/providerInstances.ts +++ b/apps/web/src/providerInstances.ts @@ -109,6 +109,23 @@ function driverKindLabel(driverKind: ProviderDriverKind): string { return PROVIDER_DISPLAY_NAMES[driverKind] ?? formatProviderDriverKindLabel(driverKind); } +/** + * Whether an instance's icon carries the account badge: accent color set, or + * several instances sharing a driver so the brand glyph alone is ambiguous. + * Shared by the composer trigger, the picker rail, and sidebar rows. + */ +export function shouldShowInstanceBadge( + entry: ProviderInstanceEntry, + entries: Iterable, +): boolean { + if (entry.accentColor) return true; + let sharedDriverCount = 0; + for (const candidate of entries) { + if (candidate.driverKind === entry.driverKind && ++sharedDriverCount > 1) return true; + } + return false; +} + export function normalizeProviderAccentColor(value: string | undefined): string | undefined { const trimmed = value?.trim(); if (!trimmed) return undefined;