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
70 changes: 53 additions & 17 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from "./ThreadStatusIndicators";
import { ProjectFavicon } from "./ProjectFavicon";
import { SessionImportDialog } from "./SessionImportDialog";
import { ProviderInstanceIcon } from "./chat/ProviderInstanceIcon";
import { useAtomValue } from "@effect/atom-react";
import { autoAnimate } from "@formkit/auto-animate";
import React, { useCallback, useEffect, memo, useMemo, useRef, useState } from "react";
Expand Down Expand Up @@ -113,6 +114,7 @@ import { readLocalApi } from "../localApi";
import { useComposerDraftStore } from "../composerDraftStore";
import { useNewThreadHandler } from "../hooks/useHandleNewThread";
import { useDesktopUpdateState } from "../state/desktopUpdate";
import { getProviderInstanceEntry } from "../providerInstances";

import { useThreadActions } from "../hooks/useThreadActions";
import { projectEnvironment } from "../state/projects";
Expand Down Expand Up @@ -208,7 +210,11 @@ import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";
import { useIsMobile } from "~/hooks/useMediaQuery";
import { CommandDialogTrigger } from "./ui/command";
import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings";
import { primaryServerConfigAtom, primaryServerKeybindingsAtom } from "../state/server";
import {
primaryServerConfigAtom,
primaryServerKeybindingsAtom,
serverEnvironment,
} from "../state/server";
import {
derivePhysicalProjectKey,
deriveProjectGroupingOverrideKey,
Expand Down Expand Up @@ -400,6 +406,11 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
reportFailure: false,
});
const environment = useEnvironment(thread.environmentId);
const serverConfig = useAtomValue(serverEnvironment.configValueAtom(thread.environmentId));
const providerInstance = getProviderInstanceEntry(
serverConfig?.providers ?? [],
thread.modelSelection.instanceId,
);
const primaryEnvironmentId = usePrimaryEnvironmentId();
const isRemoteThread =
primaryEnvironmentId !== null && thread.environmentId !== primaryEnvironmentId;
Expand Down Expand Up @@ -471,9 +482,10 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
const prStatus = prStatusIndicator(pr, gitStatus.data?.sourceControlProvider);
const terminalStatus = terminalStatusFromRunningIds(runningTerminalIds);
const isConfirmingArchive = confirmingArchiveThreadKey === threadKey && !isThreadRunning;
const hasThreadHoverControls = providerInstance !== undefined || !isThreadRunning;
const threadMetaClassName = isConfirmingArchive
? "pointer-events-none opacity-0"
: !isThreadRunning
: hasThreadHoverControls
? "pointer-events-none transition-opacity duration-150 max-sm:pr-6 group-hover/menu-sub-item:opacity-0 group-focus-within/menu-sub-item:opacity-0"
: "pointer-events-none";
const clearConfirmingArchive = useCallback(() => {
Expand Down Expand Up @@ -798,9 +810,35 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
>
Confirm
</button>
) : !isThreadRunning ? (
appSettingsConfirmThreadArchive ? (
<div className="pointer-events-none absolute top-1/2 right-0.5 -translate-y-1/2 opacity-0 transition-opacity duration-150 max-sm:pointer-events-auto max-sm:opacity-100 group-hover/menu-sub-item:pointer-events-auto group-hover/menu-sub-item:opacity-100 group-focus-within/menu-sub-item:pointer-events-auto group-focus-within/menu-sub-item:opacity-100">
) : hasThreadHoverControls ? (
<div className="pointer-events-none absolute top-1/2 right-0.5 flex -translate-y-1/2 items-center gap-0.5 opacity-0 transition-opacity duration-150 max-sm:pointer-events-auto max-sm:opacity-100 group-hover/menu-sub-item:pointer-events-auto group-hover/menu-sub-item:opacity-100 group-focus-within/menu-sub-item:pointer-events-auto group-focus-within/menu-sub-item:opacity-100">
{providerInstance ? (
<Tooltip>
<TooltipTrigger
render={
<span
role="img"
tabIndex={0}
data-thread-selection-safe
aria-label={`${providerInstance.displayName}, ${thread.modelSelection.model}`}
className="inline-flex size-7 items-center justify-center rounded-sm text-muted-foreground outline-hidden focus-visible:ring-1 focus-visible:ring-ring"
onDoubleClick={(event) => event.stopPropagation()}
/>
}
>
<ProviderInstanceIcon
driverKind={providerInstance.driverKind}
displayName={providerInstance.displayName}
accentColor={providerInstance.accentColor}
iconClassName="size-3.5"
/>
</TooltipTrigger>
<TooltipPopup side="top">
{providerInstance.displayName} · {thread.modelSelection.model}
</TooltipPopup>
</Tooltip>
) : null}
{!isThreadRunning && appSettingsConfirmThreadArchive ? (
<button
type="button"
data-thread-selection-safe
Expand All @@ -812,12 +850,10 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
>
<ArchiveIcon className="size-3.5" />
</button>
</div>
) : (
<Tooltip>
<TooltipTrigger
render={
<div className="pointer-events-none absolute top-1/2 right-0.5 -translate-y-1/2 opacity-0 transition-opacity duration-150 max-sm:pointer-events-auto max-sm:opacity-100 group-hover/menu-sub-item:pointer-events-auto group-hover/menu-sub-item:opacity-100 group-focus-within/menu-sub-item:pointer-events-auto group-focus-within/menu-sub-item:opacity-100">
) : !isThreadRunning ? (
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
data-thread-selection-safe
Expand All @@ -829,12 +865,12 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
>
<ArchiveIcon className="size-3.5" />
</button>
</div>
}
/>
<TooltipPopup side="top">Archive</TooltipPopup>
</Tooltip>
)
}
/>
<TooltipPopup side="top">Archive</TooltipPopup>
</Tooltip>
) : null}
</div>
) : null}
<span className={threadMetaClassName}>
<span className="inline-flex items-center gap-1">
Expand Down
42 changes: 42 additions & 0 deletions rev/BR-t3code-show-provider-icon-on-hover-cy-review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Cy-review: provider icon on sidebar thread hover

- Target: `t3code/show-provider-icon-on-hover`
- Base: `dev`
- Date: 2026-07-19
- Diff base: uncommitted working-tree diff from `HEAD`
- Round: 1

## Review fleet

Two reviewers were sufficient for this narrow, single-file presentation change:

- Skeptical Code Reviewer: correctness, state gaps, running-thread regressions, and missing-provider behavior.
- Accessibility and Design/Reuse Reviewer: keyboard access, tooltip semantics, responsive behavior, and reuse of existing provider abstractions.

Both reviewers were instructed to use GPT-5.6 Sol at medium reasoning effort.

## Summary

- Raw findings: 4
- Deduplicated findings: 3
- Fix now: 2
- Deferred: 1
- Discarded: 0

## Combined findings

| ID | File:line | Sources | Severity | Disposition | Rationale |
| ----- | ----------------------------------------- | ------------- | -------- | ----------- | -------------------------------------------------------------------------------------------------------------------- |
| CR-01 | `apps/web/src/components/Sidebar.tsx:484` | SC-001, AR-01 | Medium | Fix now | Running rows without a resolved provider could fade their metadata while rendering no hover replacement. |
| CR-02 | `apps/web/src/components/Sidebar.tsx:814` | AR-02 | Medium | Fix now | The provider/model tooltip trigger was not focusable, leaving the exact model unavailable to keyboard users. |
| CR-03 | `apps/web/src/components/Sidebar.tsx:409` | SC-002 | Medium | Defer | Deleted or unavailable historical provider instances have no trustworthy driver kind for branded fallback rendering. |

## Deferred candidates

| ID | Scope | Reason |
| ----- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| CR-03 | Medium | A durable fallback requires choosing whether to persist provider presentation metadata, infer it from instance ids, or show a generic historical-provider indicator. |

## Discarded summary

No findings were discarded.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Deferred items: provider icon on sidebar thread hover

## CR-03: Historical provider fallback

If a thread's provider instance is removed or its environment configuration is unavailable, the thread still retains the instance id and model but no longer has a trustworthy driver kind or display metadata for a branded icon. This means the hover affordance can be absent for those historical or disconnected rows. The current patch does not infer branding from an arbitrary instance id because custom ids do not reliably encode their driver and a generic fallback is a product choice. Follow up by deciding whether thread shells should persist provider presentation metadata or whether the sidebar should show a generic historical-provider indicator.

> cy-review complete — 2026-07-19T13:31:11+02:00 — rounds: 1
Loading