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
55 changes: 44 additions & 11 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -244,7 +248,8 @@ function SidebarThreadTooltip({
projectCwd,
projectFaviconPath,
environmentLabel,
driverKind,
providerEntry,
showInstanceBadge,
modelInstanceId,
modelLabel,
branchMismatch,
Expand All @@ -256,7 +261,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: {
Expand All @@ -266,6 +272,7 @@ function SidebarThreadTooltip({
terminalStatus: TerminalStatusIndicator | null;
terminalProcessCount: number;
}) {
const driverKind = providerEntry?.driverKind ?? null;
return (
<TooltipPopup
side="right"
Expand Down Expand Up @@ -314,10 +321,21 @@ function SidebarThreadTooltip({
<div className="flex min-w-0 items-center gap-2">
<ProviderInstanceIcon
driverKind={driverKind}
displayName={thread.session?.providerName ?? modelInstanceId}
displayName={
providerEntry?.displayName ?? thread.session?.providerName ?? modelInstanceId
}
accentColor={providerEntry?.accentColor}
// 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">{modelLabel}</div>
<div className="min-w-0 truncate text-foreground/75">
{showInstanceBadge && providerEntry
? `${modelLabel} · ${providerEntry.displayName}`
: modelLabel}
</div>
</div>
) : null}
{terminalStatus ? (
Expand Down Expand Up @@ -858,6 +876,9 @@ 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 &&
shouldShowInstanceBadge(providerEntry, props.providerEntryByInstanceId.values());
const selectedModel = providerEntry?.models.find(
(model) => model.slug === thread.modelSelection.model,
);
Expand All @@ -875,7 +896,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}
Expand Down Expand Up @@ -1453,11 +1475,19 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
</span>
) : null}
{driverKind ? (
<span className="inline-flex shrink-0 items-center opacity-60">
<span className="inline-flex shrink-0 items-center">
<ProviderInstanceIcon
driverKind={driverKind}
displayName={thread.session?.providerName ?? modelInstanceId}
iconClassName="size-3.5"
displayName={
providerEntry?.displayName ??
thread.session?.providerName ??
modelInstanceId
}
accentColor={providerEntry?.accentColor}
showBadge={showInstanceBadge}
// Glyph dims, badge stays saturated; offset matches the composer trigger.
iconClassName="size-3.5 opacity-60"
badgeClassName="right-[-0.1875rem] bottom-[-0.1875rem] h-3 min-w-3 px-0.5 text-[7px]"
/>
</span>
) : null}
Expand Down Expand Up @@ -1514,7 +1544,9 @@ 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 &&
shouldShowInstanceBadge(providerEntry, props.providerEntryByInstanceId.values());
const selectedModel = providerEntry?.models.find(
(model) => model.slug === thread.modelSelection.model,
);
Expand Down Expand Up @@ -1572,7 +1604,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}
Expand Down
19 changes: 7 additions & 12 deletions apps/web/src/components/chat/ModelPickerSidebar.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -65,14 +69,6 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
const [hoveredInstanceId, setHoveredInstanceId] = useState<ProviderInstanceId | null>(null);
const sidebarContentRef = useRef<HTMLDivElement>(null);
const [selectedIndicatorTop, setSelectedIndicatorTop] = useState<number | null>(null);
const duplicateDriverCounts = useMemo(() => {
const counts = new Map<string, number>();
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) {
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 3 additions & 5 deletions apps/web/src/components/chat/ProviderModelPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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);
Expand Down
17 changes: 17 additions & 0 deletions apps/web/src/providerInstances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProviderInstanceEntry>,
): 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;
Expand Down
Loading