History

fix(web): 33 web UI fixes (#4700)

Max Katz committed 3 months agov0.0.30-nightly.20260728.930

Commit dd5ea321 parent c13a02125 files · +544 −432Browse files

Unified · Split

25 files
  1. apps/web/src/components/CommandPaletteResults.tsx +3 −3
  2. apps/web/src/components/ProjectFavicon.tsx +7 −2
  3. apps/web/src/components/Sidebar.tsx +29 −27
  4. apps/web/src/components/SidebarV2.tsx +235 −216
  5. apps/web/src/components/chat/ChatComposer.tsx +12 −21
  6. apps/web/src/components/chat/ComposerBannerStack.tsx +2 −2
  7. apps/web/src/components/chat/ComposerCommandMenu.tsx +4 −1
  8. apps/web/src/components/chat/ComposerControl.tsx +71 −0
  9. apps/web/src/components/chat/DraftHeroHeadline.tsx +3 −3
  10. apps/web/src/components/chat/ModelListRow.tsx +1 −1
  11. apps/web/src/components/chat/ModelPickerContent.tsx +16 −10
  12. apps/web/src/components/chat/ModelPickerSidebar.tsx +15 −26
  13. apps/web/src/components/chat/ProviderModelPicker.tsx +8 −9
  14. apps/web/src/components/chat/TraitsPicker.tsx +9 −9
  15. apps/web/src/components/clerk/T3ConnectSidebarSignIn.tsx +2 −6
  16. apps/web/src/components/settings/SettingsSidebarNav.tsx +6 −22
  17. apps/web/src/components/sidebar/SidebarChrome.tsx +2 −6
  18. apps/web/src/components/ui/command.tsx +6 −6
  19. apps/web/src/components/ui/scroll-area.tsx +2 −2
  20. apps/web/src/components/ui/select.tsx +4 −2
  21. apps/web/src/components/ui/sidebar.test.tsx +23 −1
  22. apps/web/src/components/ui/sidebar.tsx +28 −19
  23. apps/web/src/contextMenuFallback.ts +2 −2
  24. apps/web/src/index.css +52 −34
  25. apps/web/src/routes/settings.tsx +2 −2
apps/web/src/components/CommandPaletteResults.tsx+3−3
@@ -41,7 +41,7 @@ export function CommandPaletteResults(props: CommandPaletteResultsProps) { · 40 unmodified lines
4141 <CommandList>
4242 {props.groups.map((group) => (
4343 <CommandGroup items={group.items} key={group.value}>
44− <CommandGroupLabel>{group.label}</CommandGroupLabel>
44+ <CommandGroupLabel className="ps-[9px]">{group.label}</CommandGroupLabel>
4545 <CommandCollection>
4646 {(item) =>
4747 item.disabled ? (
@@ -133,13 +133,13 @@ function CommandPaletteResultRow(props: { · 85 unmodified lines
133133 )}
134134 {props.item.titleTrailingContent}
135135 {props.item.timestamp ? (
136− <span className="min-w-12 shrink-0 text-right text-[10px] tabular-nums text-muted-foreground/70">
136+ <span className="min-w-12 shrink-0 text-right text-xs tabular-nums text-muted-foreground/70">
137137 {props.item.timestamp}
138138 </span>
139139 ) : null}
140140 {shortcutLabel ? <CommandShortcut>{shortcutLabel}</CommandShortcut> : null}
141141 {props.item.kind === "submenu" ? (
142− <ChevronRightIcon className="ml-auto size-4 shrink-0 text-muted-foreground/50" />
142+ <ChevronRightIcon className="-me-0.5 ms-auto size-4 shrink-0 text-muted-foreground/70" />
143143 ) : null}
144144 </CommandItem>
145145 );
apps/web/src/components/ProjectFavicon.tsx+7−2
@@ -4,6 +4,7 @@ import { FolderIcon } from "lucide-react"; · 3 unmodified lines
44 import type { ComponentType } from "react";
55 import { useState } from "react";
66 import { useAssetUrl } from "../assets/assetUrls";
7+import { cn } from "~/lib/utils";
78
89 const loadedProjectFaviconSrcs = new Set<string>();
910
@@ -40,7 +41,7 @@ function ProjectFaviconFallback({ · 30 unmodified lines
4041 readonly className?: string | undefined;
4142 readonly icon: ComponentType<{ className?: string }>;
4243 }) {
43− return <Icon className={`size-3.5 shrink-0 text-muted-foreground/50 ${className ?? ""}`} />;
44+ return <Icon className={cn("size-3.5 shrink-0 text-muted-foreground/50", className)} />;
4445 }
4546
4647 function ProjectFaviconImage({
@@ -64,7 +65,11 @@ function ProjectFaviconImage({ · 17 unmodified lines
6465 <img
6566 src={src}
6667 alt=""
67− className={`size-3.5 shrink-0 rounded-sm object-contain ${status === "loaded" ? "" : "hidden"} ${className ?? ""}`}
68+ className={cn(
69+ "size-3.5 shrink-0 rounded-sm object-contain",
70+ status !== "loaded" && "hidden",
71+ className,
72+ )}
6873 onLoad={() => {
6974 loadedProjectFaviconSrcs.add(src);
7075 setStatus("loaded");
apps/web/src/components/Sidebar.tsx+29−27
@@ -2218,9 +2218,8 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec · 2217 unmodified lines
22182218 <div className="group/project-header relative">
22192219 <SidebarMenuButton
22202220 ref={isManualProjectSorting ? dragHandleProps?.setActivatorNodeRef : undefined}
2221− size="sm"
2222− className={`h-8 gap-2 rounded-md px-2 py-1.5 pr-8 text-left hover:bg-sidebar-row-hover group-hover/project-header:bg-sidebar-row-hover group-hover/project-header:text-sidebar-foreground max-sm:pr-14 ${
2223− isManualProjectSorting ? "cursor-grab active:cursor-grabbing" : "cursor-pointer"
2221+ className={`pr-8 group-hover/project-header:bg-sidebar-row-hover group-hover/project-header:text-sidebar-foreground max-sm:pr-14 ${
2222+ isManualProjectSorting ? "cursor-grab active:cursor-grabbing" : ""
22242223 }`}
22252224 {...(isManualProjectSorting && dragHandleProps ? dragHandleProps.attributes : {})}
22262225 {...(isManualProjectSorting && dragHandleProps ? dragHandleProps.listeners : {})}
@@ -2822,30 +2821,33 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( · 595 unmodified lines
28222821 );
28232822
28242823 return (
2825− <SidebarContent className="gap-0">
2826− <SidebarGroup className="px-2 pt-2 pb-1">
2827− <SidebarMenu>
2828− <SidebarMenuItem>
2829− <CommandDialogTrigger
2830− render={
2831− <SidebarMenuButton
2832− size="sm"
2833− className="h-8 gap-2 rounded-md px-2 py-1.5 text-sidebar-muted-foreground/80 hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-0"
2834− data-testid="command-palette-trigger"
2835− />
2836− }
2837− >
2838− <SearchIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" />
2839− <span className="flex-1 truncate text-left text-sm font-medium">Search</span>
2840− {commandPaletteShortcutLabel ? (
2841− <Kbd className="h-4 min-w-0 rounded-sm px-1.5 text-[10px]">
2842− {commandPaletteShortcutLabel}
2843− </Kbd>
2844− ) : null}
2845− </CommandDialogTrigger>
2846− </SidebarMenuItem>
2847− </SidebarMenu>
2848− </SidebarGroup>
2824+ <SidebarContent
2825+ className="gap-0"
2826+ fixedHeader={
2827+ <SidebarGroup className="px-2 pt-2 pb-1">
2828+ <SidebarMenu>
2829+ <SidebarMenuItem>
2830+ <CommandDialogTrigger
2831+ render={
2832+ <SidebarMenuButton
2833+ className="focus-visible:ring-0"
2834+ data-testid="command-palette-trigger"
2835+ />
2836+ }
2837+ >
2838+ <SearchIcon />
2839+ <span className="flex-1 truncate">Search</span>
2840+ {commandPaletteShortcutLabel ? (
2841+ <Kbd className="h-4 min-w-0 rounded-sm px-1.5 text-[10px]">
2842+ {commandPaletteShortcutLabel}
2843+ </Kbd>
2844+ ) : null}
2845+ </CommandDialogTrigger>
2846+ </SidebarMenuItem>
2847+ </SidebarMenu>
2848+ </SidebarGroup>
2849+ }
2850+ >
28492851 {showArm64IntelBuildWarning && arm64IntelBuildWarningDescription ? (
28502852 <SidebarGroup className="px-2 pt-2 pb-0">
28512853 <Alert variant="warning" className="rounded-2xl border-warning/40 bg-warning/8">
apps/web/src/components/SidebarV2.tsx+235−216
@@ -242,38 +242,40 @@ function SidebarV2ThreadTooltip({ · 241 unmodified lines
242242 <TooltipPopup
243243 side="right"
244244 align="start"
245− sideOffset={8}
245+ sideOffset={4}
246246 variant="glass"
247247 className="max-w-80 text-left whitespace-normal"
248248 >
249− <div className="flex max-w-80 flex-col gap-2 p-2">
250− <div className="whitespace-nowrap text-sm font-medium text-foreground">{thread.title}</div>
251− <div className="grid gap-1.5 text-xs text-muted-foreground">
249+ <div className="flex min-w-0 max-w-80 flex-col gap-2 px-0.5 py-1.5">
250+ <div className="min-w-0 truncate text-xs leading-none font-medium text-foreground">
251+ {thread.title}
252+ </div>
253+ <div className="grid gap-1.5 pl-0.5 text-xs text-muted-foreground">
252254 {projectTitle ? (
253255 <div className="flex min-w-0 items-center gap-2">
254256 <ProjectFavicon
255257 environmentId={thread.environmentId}
256258 cwd={projectCwd ?? ""}
257− className="size-4 shrink-0 stroke-muted-foreground"
259+ className="size-3 shrink-0 stroke-muted-foreground"
258260 />
259− <div className="min-w-0 wrap-break-word text-foreground/90">{projectTitle}</div>
261+ <div className="min-w-0 truncate text-foreground/75">{projectTitle}</div>
260262 </div>
261263 ) : null}
262264 {environmentLabel ? (
263265 <div className="flex min-w-0 items-center gap-2">
264− <ServerIcon className="size-4 shrink-0 stroke-muted-foreground" />
265− <div className="min-w-0 wrap-break-word text-foreground/90">{environmentLabel}</div>
266+ <ServerIcon className="size-3 shrink-0 stroke-muted-foreground" />
267+ <div className="min-w-0 truncate text-foreground/75">{environmentLabel}</div>
266268 </div>
267269 ) : null}
268270 {thread.branch ? (
269271 <div className="flex min-w-0 items-center gap-2">
270− <GitBranchIcon className="size-4 shrink-0 stroke-muted-foreground" />
271− <div className="min-w-0 wrap-break-word text-foreground/90">{thread.branch}</div>
272+ <GitBranchIcon className="size-3 shrink-0 stroke-muted-foreground" />
273+ <div className="min-w-0 truncate text-foreground/75">{thread.branch}</div>
272274 </div>
273275 ) : null}
274276 {branchMismatch ? (
275277 <div className="flex min-w-0 items-start gap-2 text-warning">
276− <CircleAlertIcon aria-hidden className="mt-0.5 size-4 shrink-0 stroke-current" />
278+ <CircleAlertIcon aria-hidden className="mt-0.5 size-3 shrink-0 stroke-current" />
277279 <div className="min-w-0 flex-1 wrap-break-word leading-5">
278280 You're currently checked out on another branch.
279281 </div>
@@ -284,15 +286,15 @@ function SidebarV2ThreadTooltip({ · 4 unmodified lines
284286 <ProviderInstanceIcon
285287 driverKind={driverKind}
286288 displayName={thread.session?.providerName ?? modelInstanceId}
287− iconClassName="size-4 shrink-0"
289+ iconClassName="size-3 shrink-0 grayscale opacity-60"
288290 />
289− <div className="min-w-0 wrap-break-word text-foreground/90">{modelLabel}</div>
291+ <div className="min-w-0 truncate text-foreground/75">{modelLabel}</div>
290292 </div>
291293 ) : null}
292294 {thread.session?.lastError ? (
293295 <div className="flex min-w-0 items-center gap-2 text-red-600 dark:text-red-400">
294− <CircleAlertIcon className="size-4 shrink-0 stroke-current" />
295− <div className="min-w-0 wrap-break-word">{thread.session.lastError}</div>
296+ <CircleAlertIcon className="size-3 shrink-0 stroke-current" />
297+ <div className="min-w-0 truncate">Error occurred</div>
296298 </div>
297299 ) : null}
298300 </div>
@@ -815,9 +817,9 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { · 516 unmodified lines
815817 type="button"
816818 aria-label="Un-settle thread"
817819 onClick={handleUnsettleClick}
818− className="absolute inset-y-0 right-0 inline-flex cursor-pointer items-center gap-1 rounded-md bg-transparent px-2 text-xs text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/v2-row:opacity-100"
820+ className="absolute inset-y-0 right-0 -mr-1 inline-flex cursor-pointer items-center gap-1 rounded-md bg-transparent px-1.5 text-xs text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/v2-row:opacity-100"
819821 >
820− <Undo2Icon className="size-3" />
822+ <Undo2Icon className="mb-px size-3.5" />
821823 </button>
822824 ) : (
823825 <button
@@ -917,7 +919,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { · 93 unmodified lines
917919 {props.settlementSupported || showSnoozeButton ? (
918920 <span
919921 className={cn(
920− "absolute inset-y-0 right-0 flex items-stretch gap-0.5 opacity-0 transition-opacity focus-within:opacity-100 group-hover/v2-row:opacity-100",
922+ "absolute inset-y-0 right-0 flex items-stretch opacity-0 transition-opacity focus-within:opacity-100 group-hover/v2-row:opacity-100",
921923 snoozeMenuOpen && "opacity-100",
922924 )}
923925 >
@@ -933,9 +935,9 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { · 9 unmodified lines
933935 type="button"
934936 aria-label="Settle thread"
935937 onClick={handleSettleClick}
936− className="inline-flex cursor-pointer items-center gap-1 rounded-md bg-transparent px-2 text-xs text-muted-foreground hover:text-foreground"
938+ className="-mr-1 inline-flex cursor-pointer items-center gap-1 rounded-md bg-transparent px-1.5 text-xs text-muted-foreground hover:text-foreground"
937939 >
938− <CheckIcon className="size-3" />
940+ <CheckIcon className="size-3.5" />
939941 Settle
940942 </button>
941943 ) : null}
@@ -2220,151 +2222,158 @@ export default function SidebarV2() { · 1278 unmodified lines
22202222 return (
22212223 <>
22222224 <SidebarChromeHeader isElectron={isElectron} />
2223− <SidebarContent className="gap-0">
2224− <SidebarGroup className="px-2 pb-2 pt-3">
2225− <div className="flex items-center gap-1">
2226− <div className="min-w-0 flex-1">
2227− <CommandDialogTrigger
2228− render={
2229− <SidebarMenuButton
2230− size="sm"
2231− type="button"
2232− aria-label="Search threads and commands"
2233− className="h-8 gap-2 rounded-md border-0 bg-transparent px-2 py-1.5 text-sm font-medium text-sidebar-muted-foreground hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar"
2234− data-testid="command-palette-trigger"
2235− />
2236− }
2237− >
2238− <SearchIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" />
2239− <div className="flex-1 truncate text-left">Search</div>
2240− {commandPaletteShortcutLabel ? (
2241− <Kbd className="h-4 min-w-0 rounded-sm bg-sidebar-control-surface px-1.5 text-[10px] text-sidebar-muted-foreground ring-1 ring-sidebar-border">
2242− {commandPaletteShortcutLabel}
2243− </Kbd>
2244− ) : null}
2245− </CommandDialogTrigger>
2246− </div>
2247− <div className="shrink-0">
2248− <Tooltip>
2249− <TooltipTrigger
2225+ <SidebarContent
2226+ className="gap-0"
2227+ fixedHeader={
2228+ <SidebarGroup className="gap-1 p-2">
2229+ <div className="flex items-center gap-1">
2230+ <div className="min-w-0 flex-1">
2231+ <CommandDialogTrigger
22502232 render={
22512233 <SidebarMenuButton
2252− size="sm"
22532234 type="button"
2254− className="relative size-8 justify-center rounded-md border-0 bg-transparent p-0 text-sidebar-muted-foreground hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar"
2255− onClick={handleNewThreadClick}
2256− disabled={projects.length === 0}
2257− aria-label="New thread"
2235+ aria-label="Search threads and commands"
2236+ className="focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar"
2237+ data-testid="command-palette-trigger"
22582238 />
22592239 }
22602240 >
2261− <SquarePenIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" />
2262− <span
2263− className="pointer-events-none absolute left-1/2 top-1/2 size-[max(100%,3rem)] -translate-1/2 pointer-fine:hidden"
2264− aria-hidden="true"
2265− />
2266− </TooltipTrigger>
2267− <TooltipPopup side="right">
2268− {newThreadShortcutLabel ? `New thread (${newThreadShortcutLabel})` : "New thread"}
2269− </TooltipPopup>
2270− </Tooltip>
2271− </div>
2272− </div>
2273− </SidebarGroup>
2274− {projectGroups.length > 0 ? (
2275− <SidebarGroup className="px-2 pb-2 pt-0">
2276− <div className="flex items-center gap-1">
2277− <Menu open={projectScopeMenuOpen} onOpenChange={setProjectScopeMenuOpen}>
2278− <MenuTrigger
2279− aria-label="Filter threads by project"
2280− className="flex h-8 min-w-0 flex-1 cursor-pointer items-center gap-2 rounded-md px-2 text-left text-sm font-medium text-sidebar-muted-foreground outline-none hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar"
2281− >
2282− {scopedProjectGroup ? (
2283− <ProjectFavicon
2284− environmentId={scopedProjectGroup.environmentId}
2285− cwd={scopedProjectGroup.workspaceRoot}
2286− className="size-4 shrink-0"
2241+ <SearchIcon />
2242+ <div className="flex-1 truncate text-left">Search</div>
2243+ {commandPaletteShortcutLabel ? (
2244+ <Kbd className="mr-px h-4 min-w-0 rounded-sm bg-sidebar-control-surface px-1.5 text-[10px] text-sidebar-muted-foreground ring-1 ring-sidebar-border">
2245+ {commandPaletteShortcutLabel}
2246+ </Kbd>
2247+ ) : null}
2248+ </CommandDialogTrigger>
2249+ </div>
2250+ <div className="shrink-0">
2251+ <Tooltip>
2252+ <TooltipTrigger
2253+ render={
2254+ <SidebarMenuButton
2255+ size="icon"
2256+ type="button"
2257+ className="relative focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar"
2258+ onClick={handleNewThreadClick}
2259+ disabled={projects.length === 0}
2260+ aria-label="New thread"
2261+ />
2262+ }
2263+ >
2264+ <SquarePenIcon />
2265+ <span
2266+ className="pointer-events-none absolute left-1/2 top-1/2 size-[max(100%,3rem)] -translate-1/2 pointer-fine:hidden"
2267+ aria-hidden="true"
22872268 />
2288− ) : (
2289− <FolderIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" />
2290− )}
2291− <span className="min-w-0 flex-1 truncate">
2292− {scopedProjectGroup?.displayName ?? "All projects"}
2293− </span>
2294− <ChevronDownIcon className="size-4 shrink-0 text-sidebar-muted-foreground/70" />
2295− </MenuTrigger>
2296− <MenuPopup align="start" className="w-(--anchor-width)">
2297− <MenuRadioGroup
2298− value={projectScopeKey ?? "all"}
2299− onValueChange={(value) =>
2300− setProjectScopeKey(value === "all" ? null : (value as string))
2269+ </TooltipTrigger>
2270+ <TooltipPopup side="right">
2271+ {newThreadShortcutLabel
2272+ ? `New thread (${newThreadShortcutLabel})`
2273+ : "New thread"}
2274+ </TooltipPopup>
2275+ </Tooltip>
2276+ </div>
2277+ </div>
2278+ {projectGroups.length > 0 ? (
2279+ <div className="flex items-center gap-1">
2280+ <Menu open={projectScopeMenuOpen} onOpenChange={setProjectScopeMenuOpen}>
2281+ <MenuTrigger
2282+ render={
2283+ <SidebarMenuButton
2284+ aria-label="Filter threads by project"
2285+ className="min-w-0 flex-1 focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar"
2286+ />
23012287 }
23022288 >
2303− <MenuRadioItem
2304− value="all"
2305− closeOnClick
2306− className="h-8 min-h-8 px-1 py-0 text-sm font-medium [&>span:last-child]:flex [&>span:last-child]:min-w-0 [&>span:last-child]:items-center [&>span:last-child]:gap-2"
2307− >
2289+ {scopedProjectGroup ? (
2290+ <ProjectFavicon
2291+ environmentId={scopedProjectGroup.environmentId}
2292+ cwd={scopedProjectGroup.workspaceRoot}
2293+ className="size-4 shrink-0"
2294+ />
2295+ ) : (
23082296 <FolderIcon className="size-4 shrink-0" />
2309− <span className="min-w-0 truncate text-sm">All projects</span>
2310− </MenuRadioItem>
2311− {projectGroups.map((project) => {
2312− const scopeKey = project.projectKey;
2313− return (
2314− <MenuRadioItem
2315− key={scopeKey}
2316− value={scopeKey}
2317− closeOnClick
2318− className="h-8 min-h-8 px-1 py-0 text-sm font-medium [&>span:last-child]:flex [&>span:last-child]:min-w-0 [&>span:last-child]:items-center [&>span:last-child]:gap-2"
2319− >
2320− <ProjectFavicon
2321− environmentId={project.environmentId}
2322− cwd={project.workspaceRoot}
2323− className="size-4 shrink-0"
2324− />
2325− <span className="min-w-0 truncate text-sm">{project.displayName}</span>
2326− <button
2327− type="button"
2328− aria-label={`Project actions for ${project.displayName}`}
2329− title={`Project actions for ${project.displayName}`}
2330− className="ml-auto inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground/55 outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:bg-accent focus-visible:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
2331− onPointerDown={(event) => event.stopPropagation()}
2332− onClick={(event) => {
2333− void handleProjectActions(event, project);
2334− }}
2297+ )}
2298+ <span className="min-w-0 flex-1 truncate">
2299+ {scopedProjectGroup?.displayName ?? "All projects"}
2300+ </span>
2301+ <ChevronDownIcon className="-mr-px size-4 shrink-0" />
2302+ </MenuTrigger>
2303+ <MenuPopup align="start" className="w-(--anchor-width)">
2304+ <MenuRadioGroup
2305+ value={projectScopeKey ?? "all"}
2306+ onValueChange={(value) =>
2307+ setProjectScopeKey(value === "all" ? null : (value as string))
2308+ }
2309+ >
2310+ <MenuRadioItem
2311+ value="all"
2312+ closeOnClick
2313+ className="h-8 min-h-8 px-1 py-0 text-sm font-medium [&>span:last-child]:flex [&>span:last-child]:min-w-0 [&>span:last-child]:items-center [&>span:last-child]:gap-2"
2314+ >
2315+ <FolderIcon className="size-4 shrink-0" />
2316+ <span className="min-w-0 truncate text-sm">All projects</span>
2317+ </MenuRadioItem>
2318+ {projectGroups.map((project) => {
2319+ const scopeKey = project.projectKey;
2320+ return (
2321+ <MenuRadioItem
2322+ key={scopeKey}
2323+ value={scopeKey}
2324+ closeOnClick
2325+ className="h-8 min-h-8 px-1 py-0 text-sm font-medium [&>span:last-child]:flex [&>span:last-child]:min-w-0 [&>span:last-child]:items-center [&>span:last-child]:gap-2"
23352326 >
2336− <EllipsisIcon className="size-3.5" />
2337− </button>
2338− </MenuRadioItem>
2339− );
2340− })}
2341− </MenuRadioGroup>
2342− </MenuPopup>
2343− </Menu>
2344− <Tooltip>
2345− <TooltipTrigger
2346− render={
2347− <SidebarMenuButton
2348− size="sm"
2349− className="relative size-8 shrink-0 justify-center rounded-md bg-transparent p-0 text-sidebar-muted-foreground hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar"
2350− onClick={openAddProjectCommandPalette}
2351− type="button"
2352− aria-label="New project"
2327+ <ProjectFavicon
2328+ environmentId={project.environmentId}
2329+ cwd={project.workspaceRoot}
2330+ className="size-4 shrink-0"
2331+ />
2332+ <span className="min-w-0 truncate text-sm">{project.displayName}</span>
2333+ <button
2334+ type="button"
2335+ aria-label={`Project actions for ${project.displayName}`}
2336+ title={`Project actions for ${project.displayName}`}
2337+ className="ml-auto inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground/55 outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:bg-accent focus-visible:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
2338+ onPointerDown={(event) => event.stopPropagation()}
2339+ onClick={(event) => {
2340+ void handleProjectActions(event, project);
2341+ }}
2342+ >
2343+ <EllipsisIcon className="size-3.5" />
2344+ </button>
2345+ </MenuRadioItem>
2346+ );
2347+ })}
2348+ </MenuRadioGroup>
2349+ </MenuPopup>
2350+ </Menu>
2351+ <Tooltip>
2352+ <TooltipTrigger
2353+ render={
2354+ <SidebarMenuButton
2355+ size="icon"
2356+ className="relative shrink-0 focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar"
2357+ onClick={openAddProjectCommandPalette}
2358+ type="button"
2359+ aria-label="New project"
2360+ />
2361+ }
2362+ >
2363+ <FolderPlusIcon />
2364+ <span
2365+ className="pointer-events-none absolute left-1/2 top-1/2 size-[max(100%,3rem)] -translate-1/2 pointer-fine:hidden"
2366+ aria-hidden="true"
23532367 />
2354− }
2355− >
2356− <FolderPlusIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" />
2357− <span
2358− className="pointer-events-none absolute left-1/2 top-1/2 size-[max(100%,3rem)] -translate-1/2 pointer-fine:hidden"
2359− aria-hidden="true"
2360− />
2361− </TooltipTrigger>
2362− <TooltipPopup side="right">New project</TooltipPopup>
2363− </Tooltip>
2364− </div>
2368+ </TooltipTrigger>
2369+ <TooltipPopup side="right">New project</TooltipPopup>
2370+ </Tooltip>
2371+ </div>
2372+ ) : null}
23652373 </SidebarGroup>
2366− ) : null}
2367− <SidebarGroup className="min-h-0 flex-1 overflow-y-auto px-2 py-1 [scrollbar-gutter:stable]">
2374+ }
2375+ >
2376+ <SidebarGroup className="px-2 pb-1 pt-0">
23682377 <TooltipProvider
23692378 key="sidebar-thread-tooltips-150"
23702379 delay={150}
@@ -2526,12 +2535,10 @@ export default function SidebarV2() { · 155 unmodified lines
25262535 <button
25272536 type="button"
25282537 onClick={showMoreSettled}
2529− className="mt-1 flex h-[30px] w-full items-center justify-center gap-1.5 rounded-md border border-dashed border-border font-mono text-[11px] text-muted-foreground transition-colors hover:border-solid hover:border-input hover:bg-background/45 hover:text-foreground dark:border-white/15 dark:hover:border-white/30 dark:hover:bg-transparent"
2538+ className="flex h-9 w-full cursor-pointer items-center gap-2.5 rounded-md px-2.5 text-left text-sm text-sidebar-muted-foreground/55 hover:bg-sidebar-row-hover hover:text-sidebar-foreground"
25302539 >
2540+ <PlusIcon aria-hidden className="size-4 shrink-0" />
25312541 Show {Math.min(hiddenSettledCount, SETTLED_TAIL_PAGE_COUNT)} more
2532− <span className="text-muted-foreground/50">
2533− ({hiddenSettledCount} settled hidden)
2534− </span>
25352542 </button>
25362543 </li>
25372544 ) : null}
@@ -2547,7 +2554,7 @@ export default function SidebarV2() { · 9 unmodified lines
25472554 onClick={openAddProjectCommandPalette}
25482555 className="inline-flex items-center gap-1.5 rounded-md border border-sidebar-border px-2.5 py-1 text-[11px] font-medium text-sidebar-muted-foreground transition-colors hover:bg-sidebar-row-hover hover:text-sidebar-foreground"
25492556 >
2550− <PlusIcon className="size-3" />
2557+ <PlusIcon className="-mx-0.5 size-3" />
25512558 Add project
25522559 </button>
25532560 </>
@@ -2567,48 +2574,52 @@ export default function SidebarV2() { · 13 unmodified lines
25672574 }}
25682575 >
25692576 <DialogPopup className="max-w-xl">
2570− <DialogHeader className="gap-1.5">
2577+ <DialogHeader className="gap-3 pb-1!">
25712578 <DialogTitle className="text-balance">Project settings</DialogTitle>
2572− <DialogDescription>
2573− {projectActionsTarget && projectActionsTarget.memberProjects.length > 1
2574− ? `${projectActionsTarget.displayName} has an entry in each environment. Changes apply only to the entry you choose.`
2575− : `Manage ${projectActionsTarget?.displayName ?? "this project"} in this environment.`}
2579+ <DialogDescription className="sr-only">
2580+ Manage project names, grouping rules, and environments.
25762581 </DialogDescription>
2582+ <div className="grid gap-1.5 text-base text-muted-foreground">
2583+ {projectActionsTarget?.memberProjects.map((member) => (
2584+ <div key={member.physicalProjectKey} className="flex min-w-0 items-center gap-3">
2585+ <span className="flex min-w-0 items-center gap-1">
2586+ <FolderIcon className="size-3.5 shrink-0 opacity-60" />
2587+ <span className="min-w-0 truncate font-mono">{member.workspaceRoot}</span>
2588+ <Button
2589+ size="icon-xs"
2590+ variant="ghost"
2591+ className="size-4 shrink-0 rounded-sm"
2592+ aria-label="Copy project path"
2593+ title="Copy project path"
2594+ onClick={() =>
2595+ copyProjectPath(member.workspaceRoot, { path: member.workspaceRoot })
2596+ }
2597+ >
2598+ <CopyIcon className="size-3.5" />
2599+ </Button>
2600+ </span>
2601+ <span className="flex min-w-0 shrink-0 items-center gap-1">
2602+ <ServerIcon className="size-3.5 shrink-0 opacity-60" />
2603+ <span className="min-w-0 truncate">
2604+ {member.environmentLabel ?? "Current environment"}
2605+ </span>
2606+ </span>
2607+ </div>
2608+ ))}
2609+ </div>
25772610 </DialogHeader>
25782611 <DialogPanel className="p-0">
25792612 <div className="divide-y divide-border/60">
25802613 {projectActionsTarget?.memberProjects.map((member) => (
25812614 <section
25822615 key={member.physicalProjectKey}
2583− className="flex min-w-0 flex-col gap-4 px-6 py-5 sm:gap-3 sm:py-4"
2616+ className="grid min-w-0 gap-5 px-6 pb-5 pt-2 sm:gap-4 sm:pb-4 sm:pt-2"
25842617 >
2585− <div className="flex min-w-0 items-start gap-3">
2586− <ProjectFavicon
2587− environmentId={member.environmentId}
2588− cwd={member.workspaceRoot}
2589− className="size-5 shrink-0 sm:size-4"
2590− />
2591− <div className="min-w-0 flex-1">
2592− <div className="flex min-w-0 items-center gap-1.5 text-base text-muted-foreground sm:text-sm">
2593− <ServerIcon className="size-4 shrink-0 stroke-muted-foreground" />
2594− <p className="min-w-0 truncate">
2595− {member.environmentLabel ?? "Current environment"}
2596− </p>
2597− </div>
2598− <p
2599− className="truncate font-mono text-base text-muted-foreground/72 sm:text-sm"
2600− title={member.workspaceRoot}
2601− >
2602− {member.workspaceRoot}
2603− </p>
2604− </div>
2605− </div>
2606− <div className="grid gap-4 sm:grid-cols-2 sm:gap-3 sm:pl-7">
2618+ <div className="grid gap-4 sm:grid-cols-2 sm:gap-3">
26072619 <label className="grid min-w-0 gap-1.5">
26082620 <span className="font-medium text-foreground">Project name</span>
26092621 <Input
26102622 key={`${member.physicalProjectKey}:${member.title}`}
2611− size="sm"
26122623 aria-label={`Project name in ${member.environmentLabel ?? "current environment"}`}
26132624 defaultValue={member.title}
26142625 onBlur={(event) => {
@@ -2639,8 +2650,7 @@ export default function SidebarV2() { · 24 unmodified lines
26392650 }}
26402651 >
26412652 <SelectTrigger
2642− size="sm"
2643− className="w-full"
2653+ className="w-full sm:min-h-7.5"
26442654 aria-label={`Grouping rule for ${member.environmentLabel ?? "current environment"}`}
26452655 >
26462656 <SelectValue>
@@ -2672,32 +2682,23 @@ export default function SidebarV2() { · 25 unmodified lines
26722682 </Select>
26732683 </label>
26742684 </div>
2675− <div className="flex flex-wrap items-center gap-2 sm:pl-7">
2676− <Button
2677− size="sm"
2678− variant="ghost"
2679− onClick={() =>
2680− copyProjectPath(member.workspaceRoot, { path: member.workspaceRoot })
2681− }
2682− >
2683− <CopyIcon />
2684− Copy path
2685− </Button>
2686− <Button
2687− size="sm"
2688− variant="ghost"
2689− className="text-destructive-foreground hover:bg-destructive/8 hover:text-destructive-foreground sm:ml-auto"
2690− onClick={() => {
2691− const projectGroup = projectActionsTarget;
2692− if (!projectGroup) return;
2693− setProjectActionsTarget(null);
2694− void handleRemoveProjectMembers(projectGroup, [member]);
2695− }}
2696− >
2697− <Trash2Icon />
2698− Remove
2699− </Button>
2700− </div>
2685+ {projectActionsTarget.memberProjects.length > 1 ? (
2686+ <div className="flex justify-end">
2687+ <Button
2688+ size="sm"
2689+ variant="ghost"
2690+ className="text-destructive-foreground hover:bg-destructive/8 hover:text-destructive-foreground"
2691+ onClick={() => {
2692+ const projectGroup = projectActionsTarget;
2693+ setProjectActionsTarget(null);
2694+ void handleRemoveProjectMembers(projectGroup, [member]);
2695+ }}
2696+ >
2697+ <Trash2Icon />
2698+ Remove project
2699+ </Button>
2700+ </div>
2701+ ) : null}
27012702 </section>
27022703 ))}
27032704 </div>
@@ -2727,8 +2728,26 @@ export default function SidebarV2() { · 23 unmodified lines
27272728 </div>
27282729 ) : null}
27292730 </DialogPanel>
2730− <DialogFooter variant="bare">
2731− <Button onClick={() => setProjectActionsTarget(null)}>Done</Button>
2731+ <DialogFooter
2732+ variant="bare"
2733+ className={cn(
2734+ projectActionsTarget?.memberProjects.length === 1 && "sm:justify-between",
2735+ )}
2736+ >
2737+ {projectActionsTarget?.memberProjects.length === 1 ? (
2738+ <Button
2739+ variant="destructive-outline"
2740+ onClick={() => {
2741+ const projectGroup = projectActionsTarget;
2742+ setProjectActionsTarget(null);
2743+ void handleRemoveProjectMembers(projectGroup, projectGroup.memberProjects);
2744+ }}
2745+ >
2746+ <Trash2Icon />
2747+ Remove project
2748+ </Button>
2749+ ) : null}
2750+ <Button onClick={() => setProjectActionsTarget(null)}>Close</Button>
27322751 </DialogFooter>
27332752 </DialogPopup>
27342753 </Dialog>
apps/web/src/components/chat/ChatComposer.tsx+12−21
@@ -95,6 +95,7 @@ import { ComposerPrimaryActions } from "./ComposerPrimaryActions"; · 94 unmodified lines
9595 import { ComposerPendingApprovalPanel } from "./ComposerPendingApprovalPanel";
9696 import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel";
9797 import { ComposerPlanFollowUpBanner } from "./ComposerPlanFollowUpBanner";
98+import { ComposerControl, ComposerControlIcon, ComposerSelectControl } from "./ComposerControl";
9899 import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight";
99100 import { searchSlashCommandItems } from "./composerSlashCommandSearch";
100101 import {
@@ -167,7 +168,7 @@ function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children: · 66 unmodified lines
167168 );
168169 }
169170 import { Button } from "../ui/button";
170−import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
171+import { Select, SelectItem, SelectPopup, SelectValue } from "../ui/select";
171172 import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
172173 import { toastManager } from "../ui/toast";
173174 import {
@@ -303,15 +304,13 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop · 129 unmodified lines
303304 <Tooltip>
304305 <TooltipTrigger
305306 render={
306− <Button
307− variant="ghost"
307+ <ComposerControl
308308 className={cn(
309− "shrink-0 whitespace-nowrap px-2 sm:px-3",
309+ "shrink-0 whitespace-nowrap",
310310 props.interactionMode === "plan"
311311 ? "bg-blue-500/10 text-blue-400 hover:bg-blue-500/15 hover:text-blue-300"
312312 : "text-muted-foreground/70 hover:text-foreground/80",
313313 )}
314− size="sm"
315314 type="button"
316315 onClick={props.onToggleInteractionMode}
317316 aria-label={interactionModeTooltip}
@@ -319,9 +318,9 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop · 1 unmodified line
319318 }
320319 >
321320 {props.interactionMode === "plan" ? (
322− <PencilRulerIcon className="text-current opacity-100" />
321+ <ComposerControlIcon icon={PencilRulerIcon} className="text-current opacity-100" />
323322 ) : (
324− <BotIcon />
323+ <ComposerControlIcon icon={BotIcon} opticalSize="large" />
325324 )}
326325 <span className="sr-only sm:not-sr-only">
327326 {props.interactionMode === "plan" ? "Plan" : "Build"}
@@ -342,16 +341,9 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop · 14 unmodified lines
342341 onValueChange={(value) => props.onRuntimeModeChange(value!)}
343342 >
344343 <TooltipTrigger
345− render={
346− <SelectTrigger
347− variant="ghost"
348− size="sm"
349− className="font-medium"
350− aria-label="Runtime mode"
351− />
352− }
344+ render={<ComposerSelectControl className="font-medium" aria-label="Runtime mode" />}
353345 >
354− <RuntimeModeIcon className="size-4" />
346+ <ComposerControlIcon icon={RuntimeModeIcon} />
355347 <SelectValue>{runtimeModeOption.label}</SelectValue>
356348 </TooltipTrigger>
357349 <SelectPopup alignItemWithTrigger={false}>
@@ -387,22 +379,21 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop · 29 unmodified lines
387379 <Tooltip>
388380 <TooltipTrigger
389381 render={
390− <Button
391− variant="ghost"
382+ <ComposerControl
392383 className={cn(
393− "shrink-0 whitespace-nowrap px-2 sm:px-3",
384+ "shrink-0 whitespace-nowrap",
394385 props.planSidebarOpen
395386 ? "bg-blue-500/10 text-blue-400 hover:bg-blue-500/15 hover:text-blue-300"
396387 : "text-muted-foreground/70 hover:text-foreground/80",
397388 )}
398− size="sm"
399389 type="button"
400390 onClick={props.onTogglePlanSidebar}
401391 aria-label={planSidebarTooltip}
402392 />
403393 }
404394 >
405− <ListTodoIcon
395+ <ComposerControlIcon
396+ icon={ListTodoIcon}
406397 className={props.planSidebarOpen ? "text-current opacity-100" : undefined}
407398 />
408399 <span className="sr-only sm:not-sr-only">{props.planSidebarLabel}</span>
apps/web/src/components/chat/ComposerBannerStack.tsx+2−2
@@ -93,7 +93,7 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro · 92 unmodified lines
9393 {showCollapsedStackCap ? (
9494 <div
9595 className={cn(
96− "pointer-events-none absolute inset-x-0 -top-3 z-0 mx-auto h-3 rounded-t-xl",
96+ "pointer-events-none absolute inset-x-0 -top-3 z-0 mx-auto h-3 rounded-t-[22px]",
9797 "border border-b-0 border-warning/24 bg-background/96 shadow-[0_6px_18px_rgba(0,0,0,0.06)]",
9898 "transition-opacity duration-150 ease-out",
9999 "group-hover/banner-stack:opacity-0 group-focus-within/banner-stack:opacity-0",
@@ -174,7 +174,7 @@ function ComposerBannerStackAlert({ · 74 unmodified lines
174174 return (
175175 <Alert
176176 variant={item.variant}
177− className={cn("alert-glass", item.className)}
177+ className={cn("alert-glass rounded-[22px]", item.className)}
178178 data-variant={item.variant}
179179 >
180180 {item.icon}
apps/web/src/components/chat/ComposerCommandMenu.tsx+4−1
@@ -139,7 +139,10 @@ export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: { · 138 unmodified lines
139139 );
140140 }}
141141 >
142− <div ref={listRef} className="dropdown-glass relative w-full overflow-hidden rounded-[20px]">
142+ <div
143+ ref={listRef}
144+ className="dropdown-glass relative w-full overflow-hidden rounded-[20px] **:data-[slot=scroll-area-scrollbar]:data-[orientation=vertical]:my-4"
145+ >
143146 {props.items.length > 0 ? (
144147 <CommandList className="max-h-72">
145148 {groups.map((group, groupIndex) => (
apps/web/src/components/chat/ComposerControl.tsx+71−0
@@ -0,0 +1,71 @@
1+import type { ComponentProps } from "react";
2+import { ChevronDownIcon, type LucideIcon } from "lucide-react";
3+
4+import { cn } from "~/lib/utils";
5+import { Button } from "../ui/button";
6+import { SelectTrigger } from "../ui/select";
7+
8+const composerControlClassName =
9+ "h-7 min-h-7 gap-1.5 px-2.5 text-muted-foreground/70 transition-none hover:text-foreground/80 [&_svg[data-composer-control-icon]]:mx-0 [&_svg[data-composer-control-chevron]]:-mx-0.5";
10+
11+export function ComposerControl({
12+ className,
13+ size = "sm",
14+ variant = "ghost",
15+ ...props
16+}: ComponentProps<typeof Button>) {
17+ return (
18+ <Button
19+ className={cn(composerControlClassName, className)}
20+ size={size}
21+ variant={variant}
22+ {...props}
23+ />
24+ );
25+}
26+
27+export function ComposerControlIcon({
28+ icon: Icon,
29+ className,
30+ opticalSize = "default",
31+}: {
32+ icon: LucideIcon;
33+ className?: string | undefined;
34+ opticalSize?: "default" | "large";
35+}) {
36+ return (
37+ <Icon
38+ aria-hidden="true"
39+ className={cn("shrink-0", opticalSize === "large" ? "size-4.5" : "size-4", className)}
40+ data-composer-control-icon
41+ />
42+ );
43+}
44+
45+export function ComposerControlChevron() {
46+ return (
47+ <ChevronDownIcon
48+ aria-hidden="true"
49+ className="-mx-0.5 size-3.5 shrink-0 text-muted-foreground opacity-70"
50+ data-composer-control-chevron
51+ strokeWidth={2.25}
52+ />
53+ );
54+}
55+
56+export function ComposerSelectControl({
57+ className,
58+ size = "sm",
59+ variant = "ghost",
60+ ...props
61+}: ComponentProps<typeof SelectTrigger>) {
62+ return (
63+ <SelectTrigger
64+ className={cn(composerControlClassName, className)}
65+ icon={<ComposerControlChevron />}
66+ size={size}
67+ variant={variant}
68+ {...props}
69+ />
70+ );
71+}
apps/web/src/components/chat/DraftHeroHeadline.tsx+3−3
@@ -101,11 +101,11 @@ export function DraftHeroHeadline({ · 100 unmodified lines
101101 <Menu>
102102 <MenuTrigger
103103 aria-label={hasResolvedProject ? "Change project" : "Choose a project"}
104− className="pointer-events-auto inline cursor-pointer border-current border-b border-dotted text-foreground underline-offset-8 transition-opacity hover:opacity-75 focus-visible:rounded-sm focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
104+ className="pointer-events-auto inline cursor-pointer border-foreground/60 border-b border-dotted text-foreground transition-colors hover:border-foreground/80 focus-visible:rounded-sm focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
105105 >
106106 {activeProjectDisplayName ?? "Choose a project"}
107107 </MenuTrigger>
108− <MenuPopup align="center" className="max-h-80 w-64 overflow-y-auto">
108+ <MenuPopup align="center" className="max-h-80 min-w-40! w-max max-w-64 overflow-y-auto">
109109 <MenuRadioGroup
110110 value={activeProjectKey}
111111 onValueChange={(value) => {
@@ -138,7 +138,7 @@ export function DraftHeroHeadline({ · 26 unmodified lines
138138 <button
139139 type="button"
140140 onClick={openAddProject}
141− className="pointer-events-auto inline cursor-pointer border-current border-b border-dotted text-muted-foreground/60 underline-offset-8 transition-opacity hover:opacity-75 focus-visible:rounded-sm focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
141+ className="pointer-events-auto inline cursor-pointer border-muted-foreground/35 border-b border-dotted text-muted-foreground/60 transition-colors hover:border-muted-foreground/60 hover:text-muted-foreground/80 focus-visible:rounded-sm focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
142142 >
143143 {activeProjectTitle ?? "Add a project"}
144144 </button>
apps/web/src/components/chat/ModelListRow.tsx+1−1
@@ -50,7 +50,7 @@ export const ModelListRow = memo(function ModelListRow(props: { · 49 unmodified lines
5050 disabled={Boolean(props.disabledReason)}
5151 contentClassName="flex w-full items-center gap-3"
5252 className={cn(
53− "group relative w-full !min-w-0 max-w-full cursor-pointer rounded-md px-2 py-2.5 transition-[background-color,box-shadow,color]",
53+ "group relative w-full !min-w-0 max-w-full cursor-pointer rounded-md px-2 py-2 transition-[background-color,box-shadow,color]",
5454 "hover:bg-[color-mix(in_srgb,var(--popover)_90%,var(--foreground))] data-highlighted:bg-[color-mix(in_srgb,var(--popover)_90%,var(--foreground))] data-selected:bg-foreground/[0.08] data-selected:text-foreground data-selected:ring-0 [&[data-highlighted][data-selected]]:bg-[color-mix(in_srgb,var(--popover)_90%,var(--foreground))]",
5555 props.disabledReason &&
5656 "data-disabled:pointer-events-auto data-disabled:cursor-not-allowed data-disabled:hover:bg-transparent",
apps/web/src/components/chat/ModelPickerContent.tsx+16−10
@@ -43,6 +43,10 @@ type ModelPickerItem = { · 42 unmodified lines
4343
4444 const EMPTY_MODEL_JUMP_LABELS = new Map<string, string>();
4545
46+function ModelListSeparator() {
47+ return <div className="h-0.5" />;
48+}
49+
4650 // Split a `${instanceId}:${slug}` combobox key back into its pieces. Slugs
4751 // can contain colons (e.g. some vendor model ids), so we only split on the
4852 // first colon — anything after that is the slug.
@@ -521,7 +525,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { · 472 unmodified lines
521525 return (
522526 <TooltipProvider delay={0}>
523527 <div
524− className="dropdown-glass model-picker-surface relative flex h-screen max-h-96 w-screen max-w-100 flex-row overflow-hidden rounded-lg text-popover-foreground [clip-path:inset(0_round_var(--radius-lg))]"
528+ className="dropdown-glass model-picker-surface relative flex h-screen max-h-86.5 w-screen max-w-90 flex-row overflow-hidden rounded-lg text-popover-foreground [clip-path:inset(0_round_var(--radius-lg))]"
525529 data-model-picker-content="true"
526530 >
527531 {/* Sidebar */}
@@ -571,12 +575,12 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { · 43 unmodified lines
571575 <div
572576 className={cn(
573577 "flex min-h-0 flex-1 flex-col overflow-hidden bg-muted/40",
574− showSidebar && "border-l",
578+ showSidebar && "border-l border-border/70",
575579 )}
576580 >
577581 {/* Search bar */}
578− <div className="px-4 pt-2.5">
579− <div className="-translate-y-px border-b border-border/70 pb-2.5 transition-colors focus-within:border-ring">
582+ <div className="px-2 pt-2">
583+ <div className="border-b border-border/70 pb-2.5 transition-colors focus-within:border-ring">
580584 <ComboboxInput
581585 ref={searchInputRef}
582586 className="[&_input]:h-6.5 [&_input]:font-sans [&_input]:leading-6.5"
@@ -584,7 +588,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { · 1 unmodified line
584588 placeholder="Search models..."
585589 showTrigger={false}
586590 startAddon={
587− <SearchIcon className="-translate-x-0.5 size-4 shrink-0 text-muted-foreground/55" />
591+ <SearchIcon className="-translate-x-0.5 size-4 shrink-0 text-muted-foreground opacity-70" />
588592 }
589593 value={searchQuery}
590594 onChange={(e) => setSearchQuery(e.target.value)}
@@ -618,8 +622,8 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { · 27 unmodified lines
618622 </div>
619623
620624 {/* Model list */}
621− <div className="relative min-h-0 flex-1 overflow-hidden">
622− <ComboboxListVirtualized className="model-picker-list size-full min-w-0 p-0">
625+ <div className="relative min-h-0 flex-1 overflow-hidden pr-px">
626+ <ComboboxListVirtualized className="size-full min-w-0 p-0 not-empty:p-0">
623627 <LegendList<string>
624628 ref={modelListRef}
625629 data={filteredModelKeys}
@@ -656,12 +660,14 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { · 30 unmodified lines
656660 estimatedItemSize={60}
657661 drawDistance={480}
658662 recycleItems
663+ contentContainerClassName="pl-2 pr-px"
664+ ItemSeparatorComponent={ModelListSeparator}
659665 onLayout={updateModelListScrollFades}
660666 onScroll={updateModelListScrollFades}
661667 className={cn(
662− "scrollbar-gutter-both h-full overflow-x-hidden overscroll-y-contain py-1.5 [--fade-size:1.5rem]",
663− showTopScrollFade && "mask-t-from-[calc(100%-var(--fade-size))]",
664− showBottomScrollFade && "mask-b-from-[calc(100%-var(--fade-size))]",
668+ "model-picker-list h-full overflow-x-hidden overscroll-y-contain py-1.5 [--fade-size:1.5rem]",
669+ showTopScrollFade && "model-picker-list-scroll-fade-top",
670+ showBottomScrollFade && "model-picker-list-scroll-fade-bottom",
665671 )}
666672 />
667673 </ComboboxListVirtualized>
apps/web/src/components/chat/ModelPickerSidebar.tsx+15−26
@@ -78,34 +78,20 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { · 77 unmodified lines
7878 if (!content) {
7979 return;
8080 }
81− const selectedButton = Array.from(
81+ const selectedItem = Array.from(
8282 content.querySelectorAll<HTMLElement>("[data-model-picker-provider]"),
83− ).find((button) => button.dataset.modelPickerProvider === props.selectedInstanceId);
84− if (!selectedButton) {
83+ ).find((item) => item.dataset.modelPickerProvider === props.selectedInstanceId);
84+ if (!selectedItem) {
8585 setSelectedIndicatorTop(null);
8686 return;
8787 }
88− const contentRect = content.getBoundingClientRect();
89− const selectedButtonRect = selectedButton.getBoundingClientRect();
90− setSelectedIndicatorTop(
91− selectedButtonRect.top -
92− contentRect.top +
93− content.scrollTop +
94− selectedButtonRect.height / 2 -
95− 10,
96− );
88+ setSelectedIndicatorTop(selectedItem.offsetTop + selectedItem.offsetHeight / 2 - 10);
9789 }, [props.instanceEntries, props.selectedInstanceId, showFavorites]);
9890
9991 return (
100− <div
101− className="w-12 shrink-0 overflow-hidden border-r bg-muted/30"
102− data-model-picker-sidebar="true"
103− >
92+ <div className="w-11 shrink-0 overflow-hidden bg-muted/30" data-model-picker-sidebar="true">
10493 <div className="h-full overflow-y-auto overscroll-contain [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
105− <div
106− ref={sidebarContentRef}
107− className="relative flex min-h-full flex-col gap-1 px-1 pb-1 pt-0.5"
108− >
94+ <div ref={sidebarContentRef} className="relative flex min-h-full flex-col gap-1 p-1">
10995 {selectedIndicatorTop !== null ? (
11096 <div
11197 data-model-picker-selected-indicator="true"
@@ -118,8 +104,8 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { · 6 unmodified lines
118104 ) : null}
119105 {/* Favorites section */}
120106 {showFavorites ? (
121− <div className="mb-1 border-b pb-1">
122− <div className="relative w-full">
107+ <>
108+ <div className="relative w-full" data-model-picker-provider="favorites">
123109 <Tooltip>
124110 <TooltipTrigger
125111 render={
@@ -129,7 +115,6 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { · 3 unmodified lines
129115 )}
130116 onClick={() => handleSelect("favorites")}
131117 type="button"
132− data-model-picker-provider="favorites"
133118 aria-label="Favorites"
134119 >
135120 <StarIcon className="size-5 fill-current shrink-0" aria-hidden />
@@ -146,7 +131,8 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { · 10 unmodified lines
146131 </TooltipPopup>
147132 </Tooltip>
148133 </div>
149− </div>
134+ <div className="border-b border-border/70" aria-hidden="true" />
135+ </>
150136 ) : null}
151137
152138 {/* Instance buttons (one per configured instance — built-in + custom) */}
@@ -170,7 +156,6 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { · 17 unmodified lines
170156
171157 const button = (
172158 <button
173− data-model-picker-provider={entry.instanceId}
174159 className={cn(
175160 "relative isolate flex w-full cursor-pointer aspect-square items-center justify-center rounded-md transition-colors hover:bg-[color-mix(in_srgb,var(--popover)_90%,var(--foreground))] focus-visible:bg-[color-mix(in_srgb,var(--popover)_90%,var(--foreground))] focus-visible:outline-none",
176161 isDisabled && "opacity-50 cursor-not-allowed hover:bg-transparent",
@@ -228,7 +213,11 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { · 51 unmodified lines
228213 );
229214
230215 return (
231− <div key={entry.instanceId} className="relative w-full">
216+ <div
217+ key={entry.instanceId}
218+ className="relative w-full"
219+ data-model-picker-provider={entry.instanceId}
220+ >
232221 <Tooltip>
233222 <TooltipTrigger render={trigger} />
234223 <TooltipPopup
apps/web/src/components/chat/ProviderModelPicker.tsx+8−9
@@ -5,8 +5,7 @@ import { · 4 unmodified lines
55 } from "@t3tools/contracts";
66 import { memo, useEffect, useMemo, useState } from "react";
77 import type { VariantProps } from "class-variance-authority";
8−import { ChevronDownIcon } from "lucide-react";
9−import { Button, buttonVariants } from "../ui/button";
8+import { buttonVariants } from "../ui/button";
109 import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
1110 import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
1211 import { cn } from "~/lib/utils";
@@ -18,6 +17,7 @@ import { · 5 unmodified lines
1817 getTriggerDisplayModelName,
1918 } from "./providerIconUtils";
2019 import type { ProviderInstanceEntry } from "../../providerInstances";
20+import { ComposerControl, ComposerControlChevron } from "./ComposerControl";
2121
2222 export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
2323 /**
@@ -146,28 +146,27 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { · 122 unmodified lines
146146 >
147147 <PopoverTrigger
148148 render={
149− <Button
149+ <ComposerControl
150150 aria-label={props.triggerAriaLabel}
151− size="sm"
152151 variant={props.triggerVariant ?? "ghost"}
153152 data-chat-provider-model-picker="true"
154153 className={cn(
155− "min-w-0 justify-between whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80",
156− props.compact ? "max-w-42 shrink-0" : "max-w-48 shrink sm:max-w-56 sm:px-3",
154+ "min-w-0 justify-between whitespace-nowrap",
155+ props.compact ? "max-w-42 shrink-0" : "max-w-48 shrink sm:max-w-56",
157156 props.triggerClassName,
158157 )}
159158 disabled={props.disabled}
160159 />
161160 }
162161 >
163− <span className="flex min-w-0 flex-1 items-center gap-2">
162+ <span className="flex min-w-0 flex-1 items-center gap-1.5">
164163 {activeEntry ? (
165164 <ProviderInstanceIcon
166165 driverKind={activeEntry.driverKind}
167166 displayName={activeEntry.displayName}
168167 accentColor={activeEntry.accentColor}
169168 showBadge={showInstanceBadge}
170− className={showInstanceBadge ? "size-5" : "size-4"}
169+ className="size-4"
171170 iconClassName={cn("size-4", props.activeProviderIconClassName)}
172171 indicatorBackground="var(--input)"
173172 badgeClassName={cn(
@@ -184,7 +183,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { · 10 unmodified lines
184183 </Tooltip>
185184 </span>
186185 <span aria-hidden="true" className="flex items-center">
187− <ChevronDownIcon aria-hidden="true" className="!ms-0 !-me-1 size-3 shrink-0 opacity-60" />
186+ <ComposerControlChevron />
188187 </span>
189188 </PopoverTrigger>
190189 <PopoverPopup
apps/web/src/components/chat/TraitsPicker.tsx+9−9
@@ -16,8 +16,8 @@ import { · 15 unmodified lines
1616 } from "@t3tools/shared/model";
1717 import { memo, useCallback, useState } from "react";
1818 import type { VariantProps } from "class-variance-authority";
19−import { ChevronDownIcon, ZapIcon } from "lucide-react";
20−import { Button, buttonVariants } from "../ui/button";
19+import { ZapIcon } from "lucide-react";
20+import { buttonVariants } from "../ui/button";
2121 import {
2222 Menu,
2323 MenuGroup,
@@ -31,6 +31,7 @@ import { useComposerDraftStore, DraftId } from "../../composerDraftStore"; · 7 unmodified lines
3131 import { getProviderModelCapabilities } from "../../providerModels";
3232 import { cn } from "~/lib/utils";
3333 import { Badge } from "../ui/badge";
34+import { ComposerControl, ComposerControlChevron, ComposerControlIcon } from "./ComposerControl";
3435
3536 type ProviderOptions = ReadonlyArray<ProviderOptionSelection>;
3637
@@ -463,7 +464,7 @@ export const TraitsPicker = memo(function TraitsPicker({ · 426 unmodified lines
463464 });
464465 const fastModeIcon = showFastModeIcon ? (
465466 <>
466− <ZapIcon aria-hidden="true" className="size-3 shrink-0 text-foreground/80 opacity-100" />
467+ <ComposerControlIcon icon={ZapIcon} className="text-foreground/80 opacity-100" />
467468 <span className="sr-only">Fast mode on</span>
468469 </>
469470 ) : null;
@@ -479,13 +480,12 @@ export const TraitsPicker = memo(function TraitsPicker({ · 9 unmodified lines
479480 >
480481 <MenuTrigger
481482 render={
482− <Button
483− size="sm"
483+ <ComposerControl
484484 variant={triggerVariant ?? "ghost"}
485485 className={cn(
486486 isCodexStyle
487− ? "min-w-0 max-w-40 shrink justify-start overflow-hidden whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80 sm:max-w-48 sm:px-3 [&_svg]:mx-0"
488− : "shrink-0 whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80 sm:px-3",
487+ ? "min-w-0 max-w-40 shrink justify-start overflow-hidden whitespace-nowrap sm:max-w-48"
488+ : "shrink-0 whitespace-nowrap",
489489 triggerClassName,
490490 )}
491491 />
@@ -495,13 +495,13 @@ export const TraitsPicker = memo(function TraitsPicker({ · 3 unmodified lines
495495 <span className="flex min-w-0 w-full items-center gap-1.5 overflow-hidden">
496496 {fastModeIcon}
497497 <span className="min-w-0 truncate">{triggerLabel}</span>
498− <ChevronDownIcon aria-hidden="true" className="size-3 shrink-0 opacity-60" />
498+ <ComposerControlChevron />
499499 </span>
500500 ) : (
501501 <>
502502 {fastModeIcon}
503503 <span>{triggerLabel}</span>
504− <ChevronDownIcon aria-hidden="true" className="size-3 opacity-60" />
504+ <ComposerControlChevron />
505505 </>
506506 )}
507507 </MenuTrigger>
apps/web/src/components/clerk/T3ConnectSidebarSignIn.tsx+2−6
@@ -53,12 +53,8 @@ function ConfiguredT3ConnectSidebarSignIn() { · 52 unmodified lines
5353 <>
5454 <SidebarMenu>
5555 <SidebarMenuItem>
56− <SidebarMenuButton
57− size="sm"
58− className="h-8 items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-sidebar-muted-foreground/80 hover:bg-sidebar-row-hover hover:text-sidebar-foreground"
59− onClick={openAuthPrompt}
60− >
61− <LogInIcon className="size-4 shrink-0" />
56+ <SidebarMenuButton onClick={openAuthPrompt}>
57+ <LogInIcon />
6258 <span>Sign in to T3 Connect</span>
6359 </SidebarMenuButton>
6460 </SidebarMenuItem>
apps/web/src/components/settings/SettingsSidebarNav.tsx+6−22
@@ -72,7 +72,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { · 71 unmodified lines
7272 return (
7373 <>
7474 <SidebarContent className="overflow-x-hidden">
75− <SidebarGroup className="px-2 py-3">
75+ <SidebarGroup className="p-2">
7676 <SidebarMenu>
7777 {SETTINGS_NAV_ITEMS.map((item) => {
7878 const Icon = item.icon;
@@ -80,22 +80,10 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { · 1 unmodified line
8080 return (
8181 <SidebarMenuItem key={item.to}>
8282 <SidebarMenuButton
83− size="sm"
8483 isActive={isActive}
85− className={
86− isActive
87− ? "h-8 items-center gap-2 rounded-md bg-sidebar-row-active px-2 py-1.5 text-left text-sm font-medium text-sidebar-foreground"
88− : "h-8 items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm font-medium text-sidebar-muted-foreground/80 hover:bg-sidebar-row-hover hover:text-sidebar-foreground"
89− }
9084 onClick={() => handleSectionClick(item.to)}
9185 >
92− <Icon
93− className={
94− isActive
95− ? "size-4 shrink-0 text-sidebar-foreground"
96− : "size-4 shrink-0 text-sidebar-muted-foreground/60"
97− }
98− />
86+ <Icon />
9987 <span className="truncate">{item.label}</span>
10088 </SidebarMenuButton>
10189 </SidebarMenuItem>
@@ -106,15 +94,11 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { · 4 unmodified lines
10694 </SidebarContent>
10795 <SidebarFooter className="p-2">
10896 <T3ConnectSidebarSignIn />
109− <div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-1">
110− <SidebarMenu className="min-w-0">
97+ <div className="flex items-center gap-1">
98+ <SidebarMenu className="min-w-0 flex-1">
11199 <SidebarMenuItem>
112− <SidebarMenuButton
113− size="sm"
114− className="h-8 items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-sidebar-muted-foreground/80 hover:bg-sidebar-row-hover hover:text-sidebar-foreground"
115− onClick={handleBackClick}
116− >
117− <ArrowLeftIcon className="size-4" />
100+ <SidebarMenuButton onClick={handleBackClick}>
101+ <ArrowLeftIcon />
118102 <span>Back</span>
119103 </SidebarMenuButton>
120104 </SidebarMenuItem>
apps/web/src/components/sidebar/SidebarChrome.tsx+2−6
@@ -113,12 +113,8 @@ export const SidebarChromeFooter = memo(function SidebarChromeFooter() { · 112 unmodified lines
113113 <SidebarUpdatePill />
114114 <SidebarMenu>
115115 <SidebarMenuItem>
116− <SidebarMenuButton
117− size="sm"
118− className="h-8 items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-sidebar-muted-foreground/80 hover:bg-sidebar-row-hover hover:text-sidebar-foreground"
119− onClick={handleSettingsClick}
120− >
121− <SettingsIcon className="size-4.5 shrink-0" />
116+ <SidebarMenuButton onClick={handleSettingsClick}>
117+ <SettingsIcon />
122118 <span>Settings</span>
123119 </SidebarMenuButton>
124120 </SidebarMenuItem>
apps/web/src/components/ui/command.tsx+6−6
@@ -65,7 +65,7 @@ function CommandDialogPopup({ · 64 unmodified lines
6565 <CommandDialogPrimitive.Popup
6666 className={cn(
6767 DIALOG_POPUP_CLASS,
68− "pointer-events-auto max-h-105 max-w-xl text-foreground **:data-[slot=scroll-area-viewport]:data-has-overflow-y:pe-1",
68+ "pointer-events-auto max-h-105 max-w-xl text-foreground",
6969 className,
7070 )}
7171 data-slot="command-dialog-popup"
@@ -103,16 +103,16 @@ function CommandInput({ · 31 unmodified lines
103103 wrapperClassName?: string | undefined;
104104 }) {
105105 return (
106− <div className={cn("px-2.5 py-1.5", wrapperClassName)}>
106+ <div className={cn("px-1.25 py-1.5", wrapperClassName)}>
107107 <AutocompleteInput
108108 autoFocus
109109 className={cn(
110− "border-transparent! bg-transparent! shadow-none before:hidden has-focus-visible:ring-0 placeholder:text-muted-foreground/80",
110+ "border-transparent! bg-transparent! shadow-none before:hidden has-focus-visible:ring-0 placeholder:text-muted-foreground/80 *:data-[slot=autocomplete-input]:ps-9! sm:*:data-[slot=autocomplete-input]:ps-8.5!",
111111 className,
112112 )}
113113 placeholder={placeholder}
114114 size="lg"
115− startAddon={<SearchIcon />}
115+ startAddon={<SearchIcon className="translate-x-0.5 text-muted-foreground" />}
116116 {...props}
117117 />
118118 </div>
@@ -198,7 +198,7 @@ function CommandShortcut({ className, ...props }: React.ComponentProps<"kbd">) { · 79 unmodified lines
198198 return (
199199 <kbd
200200 className={cn(
201− "ms-auto font-medium font-sans text-muted-foreground text-xs tracking-widest",
201+ "ms-auto font-medium font-sans text-muted-foreground/70 text-xs tracking-widest",
202202 className,
203203 )}
204204 data-slot="command-shortcut"
@@ -211,7 +211,7 @@ function CommandFooter({ className, ...props }: React.ComponentProps<"div">) { · 6 unmodified lines
211211 return (
212212 <div
213213 className={cn(
214− "relative flex items-center justify-between gap-2 rounded-b-[calc(var(--radius-2xl)-1px)] bg-foreground/[0.025] px-5 py-3 font-medium text-sm text-muted-foreground [&_[data-slot=kbd-group]]:font-sans [&_[data-slot=kbd]]:bg-foreground/[0.08] [&_[data-slot=kbd]]:text-foreground [&_[data-slot=kbd]]:ring-0",
214+ "relative flex items-center justify-between gap-2 rounded-b-[calc(var(--radius-2xl)-1px)] bg-foreground/[0.025] px-4 py-2.5 font-medium text-sm text-muted-foreground [&_[data-slot=kbd-group]]:font-sans [&_[data-slot=kbd]]:bg-foreground/[0.08] [&_[data-slot=kbd]]:text-foreground [&_[data-slot=kbd]]:ring-0",
215215 className,
216216 )}
217217 data-slot="command-footer"
apps/web/src/components/ui/scroll-area.tsx+2−2
@@ -56,7 +56,7 @@ function ScrollBar({ · 55 unmodified lines
5656 return (
5757 <ScrollAreaPrimitive.Scrollbar
5858 className={cn(
59− "m-1 flex opacity-0 transition-opacity delay-300 data-[orientation=horizontal]:h-1.5 data-[orientation=vertical]:w-1.5 data-[orientation=horizontal]:flex-col data-hovering:opacity-100 data-scrolling:opacity-100 data-hovering:delay-0 data-scrolling:delay-0 data-hovering:duration-100 data-scrolling:duration-100",
59+ "flex opacity-0 transition-opacity delay-300 data-[orientation=horizontal]:mx-1 data-[orientation=horizontal]:mb-px data-[orientation=horizontal]:h-1.5 data-[orientation=vertical]:my-1 data-[orientation=vertical]:mr-px data-[orientation=vertical]:w-1.5 data-[orientation=horizontal]:flex-col data-hovering:opacity-100 data-scrolling:opacity-100 data-hovering:delay-0 data-scrolling:delay-0 data-hovering:duration-100 data-scrolling:duration-100",
6060 className,
6161 )}
6262 data-slot="scroll-area-scrollbar"
@@ -64,7 +64,7 @@ function ScrollBar({ · 1 unmodified line
6464 {...props}
6565 >
6666 <ScrollAreaPrimitive.Thumb
67− className="relative flex-1 rounded-full bg-foreground/20"
67+ className="relative flex-1 rounded-full bg-[var(--app-scrollbar-thumb)] transition-colors hover:bg-[var(--app-scrollbar-thumb-hover)]"
6868 data-slot="scroll-area-thumb"
6969 />
7070 </ScrollAreaPrimitive.Scrollbar>
apps/web/src/components/ui/select.tsx+4−2
@@ -77,8 +77,10 @@ function SelectTrigger({ · 76 unmodified lines
7777 size = "default",
7878 variant = "default",
7979 children,
80+ icon,
8081 ...props
81−}: SelectPrimitive.Trigger.Props & VariantProps<typeof selectTriggerVariants>) {
82+}: SelectPrimitive.Trigger.Props &
83+ VariantProps<typeof selectTriggerVariants> & { icon?: React.ReactNode }) {
8284 return (
8385 <SelectPrimitive.Trigger
8486 className={cn(selectTriggerVariants({ size, variant }), className)}
@@ -87,7 +89,7 @@ function SelectTrigger({ · 2 unmodified lines
8789 >
8890 {children}
8991 <SelectPrimitive.Icon data-slot="select-icon">
90− <ChevronDownIcon className="-me-1 size-3 opacity-50" />
92+ {icon ?? <ChevronDownIcon className="-me-1 size-3 opacity-50" />}
9193 </SelectPrimitive.Icon>
9294 </SelectPrimitive.Trigger>
9395 );
apps/web/src/components/ui/sidebar.test.tsx+23−1
@@ -50,13 +50,35 @@ describe("sidebar interactive cursors", () => { · 49 unmodified lines
5050 expect(html).toContain("size-[var(--workspace-titlebar-control-size)]!");
5151 });
5252
53− it("uses a pointer cursor for menu buttons by default", () => {
53+ it("uses shared geometry and icon constraints for menu buttons by default", () => {
5454 const html = renderSidebarButton();
5555
5656 expect(html).toContain('data-slot="sidebar-menu-button"');
57+ expect(html).toContain("h-8");
58+ expect(html).toContain("rounded-md");
59+ expect(html).toContain("px-2");
60+ expect(html).toContain("py-1.5");
61+ expect(html).toContain("]:size-4");
62+ expect(html).toContain("]:shrink-0");
5763 expect(html).toContain("cursor-pointer");
5864 });
5965
66+ it("applies the shared default treatment to icon-only menu buttons", () => {
67+ const html = renderToStaticMarkup(
68+ <SidebarProvider>
69+ <SidebarMenuButton size="icon">
70+ <span>+</span>
71+ </SidebarMenuButton>
72+ </SidebarProvider>,
73+ );
74+
75+ expect(html).toContain("size-8");
76+ expect(html).toContain("justify-center");
77+ expect(html).toContain("p-0");
78+ expect(html).toContain("font-medium");
79+ expect(html).toContain("text-sidebar-muted-foreground/80");
80+ });
81+
6082 it("lets project drag handles override the default pointer cursor", () => {
6183 const html = renderSidebarButton("cursor-grab");
6284
apps/web/src/components/ui/sidebar.tsx+28−19
@@ -684,19 +684,28 @@ function SidebarSeparator({ className, ...props }: React.ComponentProps<typeof S · 683 unmodified lines
684684 );
685685 }
686686
687−function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
687+function SidebarContent({
688+ className,
689+ fixedHeader,
690+ ...props
691+}: React.ComponentProps<"div"> & {
692+ fixedHeader?: React.ReactNode;
693+}) {
688694 return (
689− <ScrollArea hideScrollbars scrollFade className="h-auto min-h-0 flex-1">
690− <div
691− className={cn(
692− "flex w-full min-w-0 flex-col gap-2 group-data-[collapsible=icon]:overflow-hidden",
693− className,
694− )}
695− data-sidebar="content"
696− data-slot="sidebar-content"
697− {...props}
698− />
699− </ScrollArea>
695+ <>
696+ {fixedHeader ? <div className="w-full shrink-0">{fixedHeader}</div> : null}
697+ <ScrollArea hideScrollbars scrollFade className="h-auto min-h-0 flex-1">
698+ <div
699+ className={cn(
700+ "flex w-full min-w-0 flex-col gap-2 group-data-[collapsible=icon]:overflow-hidden",
701+ className,
702+ )}
703+ data-sidebar="content"
704+ data-slot="sidebar-content"
705+ {...props}
706+ />
707+ </ScrollArea>
708+ </>
700709 );
701710 }
702711
@@ -783,7 +792,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { · 80 unmodified lines
783792 }
784793
785794 const sidebarMenuButtonVariants = cva(
786− "peer/menu-button flex w-full cursor-pointer items-center gap-2 overflow-hidden rounded-lg p-2 text-left text-sm outline-hidden ring-ring transition-[width,height,padding] hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 active:bg-sidebar-row-active active:text-sidebar-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pe-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-row-selected data-[active=true]:font-medium data-[active=true]:text-sidebar-foreground data-[state=open]:hover:bg-sidebar-row-hover data-[state=open]:hover:text-sidebar-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0",
795+ "peer/menu-button flex w-full cursor-pointer items-center gap-2 overflow-hidden text-left outline-hidden ring-ring transition-[width,height,padding] hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 active:bg-sidebar-row-active active:text-sidebar-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pe-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-row-selected data-[active=true]:font-medium data-[active=true]:text-sidebar-foreground data-[state=open]:hover:bg-sidebar-row-hover data-[state=open]:hover:text-sidebar-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-muted-foreground [&>svg]:opacity-60 hover:[&>svg]:text-sidebar-foreground hover:[&>svg]:opacity-100 active:[&>svg]:text-sidebar-foreground active:[&>svg]:opacity-100 data-[active=true]:[&>svg]:text-sidebar-foreground data-[active=true]:[&>svg]:opacity-100",
787796 {
788797 defaultVariants: {
789798 size: "default",
@@ -791,14 +800,14 @@ const sidebarMenuButtonVariants = cva( · 1 unmodified line
791800 },
792801 variants: {
793802 size: {
794− default: "h-8 text-sm",
795− lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
796− sm: "h-7 text-xs",
803+ default: "h-8 rounded-md px-2.5 py-1.5 text-sm",
804+ icon: "size-8 justify-center rounded-md p-0",
805+ lg: "h-12 rounded-lg p-2 text-sm group-data-[collapsible=icon]:p-0!",
806+ sm: "h-7 rounded-lg p-2 text-xs",
797807 },
798808 variant: {
799− default: "hover:bg-sidebar-row-hover hover:text-sidebar-foreground",
800− outline:
801− "bg-sidebar-control-surface ring-1 ring-sidebar-border hover:bg-sidebar-row-hover hover:text-sidebar-foreground",
809+ default: "font-medium text-sidebar-muted-foreground/80",
810+ outline: "bg-sidebar-control-surface ring-1 ring-sidebar-border",
802811 },
803812 },
804813 },
apps/web/src/contextMenuFallback.ts+2−2
@@ -166,9 +166,9 @@ export function showContextMenuFallback<T extends string>( · 165 unmodified lines
166166
167167 const menu = document.createElement("div");
168168 menu.className =
169− "fixed z-[10000] min-w-32 max-w-sm overflow-hidden rounded-lg border border-border bg-popover bg-clip-padding text-popover-foreground shadow-lg/5 outline-none";
169+ "dropdown-glass fixed z-[10000] min-w-32 max-w-sm overflow-hidden rounded-lg bg-clip-padding text-popover-foreground outline-none";
170170 menu.style.cssText =
171− "position:fixed;z-index:10000;min-width:8rem;max-width:24rem;overflow:hidden;border-radius:var(--radius-lg);border:1px solid var(--border);background:var(--popover);background-clip:padding-box;color:var(--popover-foreground);box-shadow:0 10px 15px -3px rgb(0 0 0 / 0.05),0 4px 6px -4px rgb(0 0 0 / 0.05);outline:none;pointer-events:auto;";
171+ "position:fixed;z-index:10000;min-width:8rem;max-width:24rem;overflow:hidden;border-radius:var(--radius-lg);background-clip:padding-box;color:var(--popover-foreground);outline:none;pointer-events:auto;";
172172 menu.style.left = `${preferredLeft}px`;
173173 menu.style.top = `${preferredTop}px`;
174174 menu.dataset.level = String(level);
apps/web/src/index.css+52−34
@@ -77,6 +77,8 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil · 76 unmodified lines
7777
7878 :root {
7979 --app-scrollbar-width: 6px;
80+ --app-scrollbar-thumb: rgb(217 217 217);
81+ --app-scrollbar-thumb-hover: rgb(191 191 191);
8082 --glass-blur: 12px;
8183 --glass-opacity: 80%;
8284 --glass-saturation: 1.14;
@@ -91,6 +93,8 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil · 8 unmodified lines
9193 }
9294
9395 .dark {
96+ --app-scrollbar-thumb: rgb(255 255 255 / 8%);
97+ --app-scrollbar-thumb-hover: rgb(255 255 255 / 12%);
9498 --glass-blur: 16px;
9599 --glass-saturation: 1.08;
96100 }
@@ -327,12 +331,13 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil · 230 unmodified lines
327331 rgb(0 0 0 / 96%) 82%,
328332 black 100%
329333 ),
330− linear-gradient(black, black);
331− -webkit-mask-position: top, bottom;
334+ linear-gradient(black, black), linear-gradient(black, black);
335+ -webkit-mask-position: top, bottom, right;
332336 -webkit-mask-repeat: no-repeat;
333337 -webkit-mask-size:
334338 100% var(--topbar-scroll-fade-height),
335− 100% calc(100% - var(--topbar-scroll-fade-height));
339+ 100% calc(100% - var(--topbar-scroll-fade-height)),
340+ var(--app-scrollbar-width) 100%;
336341 mask-image:
337342 linear-gradient(
338343 to bottom,
@@ -344,12 +349,13 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil · 5 unmodified lines
344349 rgb(0 0 0 / 96%) 82%,
345350 black 100%
346351 ),
347− linear-gradient(black, black);
348− mask-position: top, bottom;
352+ linear-gradient(black, black), linear-gradient(black, black);
353+ mask-position: top, bottom, right;
349354 mask-repeat: no-repeat;
350355 mask-size:
351356 100% var(--topbar-scroll-fade-height),
352− 100% calc(100% - var(--topbar-scroll-fade-height));
357+ 100% calc(100% - var(--topbar-scroll-fade-height)),
358+ var(--app-scrollbar-width) 100%;
353359 }
354360
355361 .workspace-titlebar-controls {
@@ -1035,20 +1041,54 @@ code { · 679 unmodified lines
10351041 }
10361042
10371043 ::-webkit-scrollbar-thumb {
1038− background: rgba(0, 0, 0, 0.15);
1044+ background: var(--app-scrollbar-thumb);
10391045 border-radius: 3px;
10401046 }
10411047
10421048 ::-webkit-scrollbar-thumb:hover {
1043− background: rgba(0, 0, 0, 0.25);
1049+ background: var(--app-scrollbar-thumb-hover);
10441050 }
10451051
1046−.dark ::-webkit-scrollbar-thumb {
1047− background: rgba(255, 255, 255, 0.1);
1052+.model-picker-list::-webkit-scrollbar-track {
1053+ margin-block: 0.5rem;
10481054 }
10491055
1050−.dark ::-webkit-scrollbar-thumb:hover {
1051− background: rgba(255, 255, 255, 0.18);
1056+.model-picker-list-scroll-fade-top,
1057+.model-picker-list-scroll-fade-bottom {
1058+ -webkit-mask-image: var(--model-picker-list-scroll-mask), linear-gradient(black, black);
1059+ -webkit-mask-position: left, right;
1060+ -webkit-mask-repeat: no-repeat;
1061+ -webkit-mask-size:
1062+ calc(100% - var(--app-scrollbar-width)) 100%,
1063+ var(--app-scrollbar-width) 100%;
1064+ mask-image: var(--model-picker-list-scroll-mask), linear-gradient(black, black);
1065+ mask-position: left, right;
1066+ mask-repeat: no-repeat;
1067+ mask-size:
1068+ calc(100% - var(--app-scrollbar-width)) 100%,
1069+ var(--app-scrollbar-width) 100%;
1070+}
1071+
1072+.model-picker-list-scroll-fade-top {
1073+ --model-picker-list-scroll-mask: linear-gradient(to bottom, transparent, black var(--fade-size));
1074+}
1075+
1076+.model-picker-list-scroll-fade-bottom {
1077+ --model-picker-list-scroll-mask: linear-gradient(
1078+ to bottom,
1079+ black calc(100% - var(--fade-size)),
1080+ transparent
1081+ );
1082+}
1083+
1084+.model-picker-list-scroll-fade-top.model-picker-list-scroll-fade-bottom {
1085+ --model-picker-list-scroll-mask: linear-gradient(
1086+ to bottom,
1087+ transparent,
1088+ black var(--fade-size),
1089+ black calc(100% - var(--fade-size)),
1090+ transparent
1091+ );
10521092 }
10531093
10541094 .turn-chip-strip {
@@ -1569,28 +1609,6 @@ label:has(> select#reasoning-effort) select { · 514 unmodified lines
15691609 animation: ultrathink-rainbow 10s linear infinite;
15701610 }
15711611
1572−/* Thin scrollbar for model picker */
1573−.model-picker-list::-webkit-scrollbar {
1574− width: 4px;
1575−}
1576−
1577−.model-picker-list::-webkit-scrollbar-thumb {
1578− background: rgba(0, 0, 0, 0.1);
1579− border-radius: 2px;
1580−}
1581−
1582−.model-picker-list::-webkit-scrollbar-thumb:hover {
1583− background: rgba(0, 0, 0, 0.2);
1584−}
1585−
1586−.dark .model-picker-list::-webkit-scrollbar-thumb {
1587− background: rgba(255, 255, 255, 0.08);
1588−}
1589−
1590−.dark .model-picker-list::-webkit-scrollbar-thumb:hover {
1591− background: rgba(255, 255, 255, 0.15);
1592−}
1593−
15941612 /* Composer chips are non-editable decorators, so the browser skips them when
15951613 painting text selection; this overlay stands in for the native highlight. */
15961614 .composer-inline-chip[data-composer-chip-selected]::after {
apps/web/src/routes/settings.tsx+2−2
@@ -74,11 +74,11 @@ function SettingsContentLayout() { · 73 unmodified lines
7474 {!isElectron && (
7575 <header
7676 className={cn(
77− "px-3 py-2 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5",
77+ "workspace-topbar px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5",
7878 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
7979 )}
8080 >
81− <div className="flex min-h-7 items-center gap-2 sm:min-h-6">
81+ <div className="flex w-full items-center gap-2">
8282 <span className="text-sm font-medium text-foreground">Settings</span>
8383 {showRestoreDefaults ? (
8484 <div className="ms-auto flex items-center gap-2">

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t