From dd5ea324885aea4980a49bb42d28f83023c40ea7 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 28 Jul 2026 08:43:49 +0400 Subject: [PATCH] fix(web): 33 web UI fixes (#4700) --- .../src/components/CommandPaletteResults.tsx | 6 +- apps/web/src/components/ProjectFavicon.tsx | 9 +- apps/web/src/components/Sidebar.tsx | 56 +-- apps/web/src/components/SidebarV2.tsx | 459 +++++++++--------- apps/web/src/components/chat/ChatComposer.tsx | 33 +- .../components/chat/ComposerBannerStack.tsx | 4 +- .../components/chat/ComposerCommandMenu.tsx | 5 +- .../src/components/chat/ComposerControl.tsx | 71 +++ .../src/components/chat/DraftHeroHeadline.tsx | 6 +- apps/web/src/components/chat/ModelListRow.tsx | 2 +- .../components/chat/ModelPickerContent.tsx | 26 +- .../components/chat/ModelPickerSidebar.tsx | 41 +- .../components/chat/ProviderModelPicker.tsx | 17 +- apps/web/src/components/chat/TraitsPicker.tsx | 18 +- .../clerk/T3ConnectSidebarSignIn.tsx | 8 +- .../settings/SettingsSidebarNav.tsx | 28 +- .../src/components/sidebar/SidebarChrome.tsx | 8 +- apps/web/src/components/ui/command.tsx | 12 +- apps/web/src/components/ui/scroll-area.tsx | 4 +- apps/web/src/components/ui/select.tsx | 6 +- apps/web/src/components/ui/sidebar.test.tsx | 24 +- apps/web/src/components/ui/sidebar.tsx | 47 +- apps/web/src/contextMenuFallback.ts | 4 +- apps/web/src/index.css | 86 ++-- apps/web/src/routes/settings.tsx | 4 +- 25 files changed, 548 insertions(+), 436 deletions(-) create mode 100644 apps/web/src/components/chat/ComposerControl.tsx diff --git a/apps/web/src/components/CommandPaletteResults.tsx b/apps/web/src/components/CommandPaletteResults.tsx index d4d31af36828d46b050b201baa21f7e86d381de2..532d546df9a3c850368057fd80dfc7704dce3252 100644 --- a/apps/web/src/components/CommandPaletteResults.tsx +++ b/apps/web/src/components/CommandPaletteResults.tsx @@ -41,7 +41,7 @@ export function CommandPaletteResults(props: CommandPaletteResultsProps) { {props.groups.map((group) => ( - {group.label} + {group.label} {(item) => item.disabled ? ( @@ -133,13 +133,13 @@ function CommandPaletteResultRow(props: { )} {props.item.titleTrailingContent} {props.item.timestamp ? ( - + {props.item.timestamp} ) : null} {shortcutLabel ? {shortcutLabel} : null} {props.item.kind === "submenu" ? ( - + ) : null} ); diff --git a/apps/web/src/components/ProjectFavicon.tsx b/apps/web/src/components/ProjectFavicon.tsx index bc3e8ee832ff14b3e4ffc35a9bfef83224caee32..201241731fae120e4cf507e7a32ffe8cd6392eb3 100644 --- a/apps/web/src/components/ProjectFavicon.tsx +++ b/apps/web/src/components/ProjectFavicon.tsx @@ -4,6 +4,7 @@ import { FolderIcon } from "lucide-react"; import type { ComponentType } from "react"; import { useState } from "react"; import { useAssetUrl } from "../assets/assetUrls"; +import { cn } from "~/lib/utils"; const loadedProjectFaviconSrcs = new Set(); @@ -40,7 +41,7 @@ function ProjectFaviconFallback({ readonly className?: string | undefined; readonly icon: ComponentType<{ className?: string }>; }) { - return ; + return ; } function ProjectFaviconImage({ @@ -64,7 +65,11 @@ function ProjectFaviconImage({ { loadedProjectFaviconSrcs.add(src); setStatus("loaded"); diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index b05b3a39d90450961876f5b3860ed531a6410792..98b5dcf84edbc397c801d0860d017d1c47927ea4 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -2218,9 +2218,8 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
- - - - - } - > - - Search - {commandPaletteShortcutLabel ? ( - - {commandPaletteShortcutLabel} - - ) : null} - - - - + + + + + } + > + + Search + {commandPaletteShortcutLabel ? ( + + {commandPaletteShortcutLabel} + + ) : null} + + + + + } + > {showArm64IntelBuildWarning && arm64IntelBuildWarningDescription ? ( diff --git a/apps/web/src/components/SidebarV2.tsx b/apps/web/src/components/SidebarV2.tsx index ed9eadd907b7f8badbe50ba2e0d53cade59fcfc4..3d314fd1ac2a704a893cd968b3e6f783bb73bd4b 100644 --- a/apps/web/src/components/SidebarV2.tsx +++ b/apps/web/src/components/SidebarV2.tsx @@ -242,38 +242,40 @@ function SidebarV2ThreadTooltip({ -
-
{thread.title}
-
+
+
+ {thread.title} +
+
{projectTitle ? (
-
{projectTitle}
+
{projectTitle}
) : null} {environmentLabel ? (
- -
{environmentLabel}
+ +
{environmentLabel}
) : null} {thread.branch ? (
- -
{thread.branch}
+ +
{thread.branch}
) : null} {branchMismatch ? (
- +
You're currently checked out on another branch.
@@ -284,15 +286,15 @@ function SidebarV2ThreadTooltip({ -
{modelLabel}
+
{modelLabel}
) : null} {thread.session?.lastError ? (
- -
{thread.session.lastError}
+ +
Error occurred
) : null}
@@ -815,9 +817,9 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { type="button" aria-label="Un-settle thread" onClick={handleUnsettleClick} - 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" + 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" > - + ) : ( ) : null} @@ -2220,151 +2222,158 @@ export default function SidebarV2() { return ( <> - - -
-
- - } - > - -
Search
- {commandPaletteShortcutLabel ? ( - - {commandPaletteShortcutLabel} - - ) : null} -
-
-
- - - } - > - - - - {newThreadShortcutLabel ? `New thread (${newThreadShortcutLabel})` : "New thread"} - - -
-
-
- {projectGroups.length > 0 ? ( - +
- - + + } > - {scopedProjectGroup ? ( - +
Search
+ {commandPaletteShortcutLabel ? ( + + {commandPaletteShortcutLabel} + + ) : null} +
+
+
+ + + } + > + + + + {newThreadShortcutLabel + ? `New thread (${newThreadShortcutLabel})` + : "New thread"} + + +
+
+ {projectGroups.length > 0 ? ( +
+ + } > - + {scopedProjectGroup ? ( + + ) : ( - All projects - - {projectGroups.map((project) => { - const scopeKey = project.projectKey; - return ( - - - {project.displayName} - - - ); - })} - - - - - + {project.displayName} + + + ); + })} + + + + + + } + > + + - New project - -
+ + New project + +
+ ) : null} - ) : null} - + } + > + + Show {Math.min(hiddenSettledCount, SETTLED_TAIL_PAGE_COUNT)} more - - ({hiddenSettledCount} settled hidden) - ) : null} @@ -2547,7 +2554,7 @@ export default function SidebarV2() { onClick={openAddProjectCommandPalette} 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" > - + Add project @@ -2567,48 +2574,52 @@ export default function SidebarV2() { }} > - + Project settings - - {projectActionsTarget && projectActionsTarget.memberProjects.length > 1 - ? `${projectActionsTarget.displayName} has an entry in each environment. Changes apply only to the entry you choose.` - : `Manage ${projectActionsTarget?.displayName ?? "this project"} in this environment.`} + + Manage project names, grouping rules, and environments. +
+ {projectActionsTarget?.memberProjects.map((member) => ( +
+ + + {member.workspaceRoot} + + + + + + {member.environmentLabel ?? "Current environment"} + + +
+ ))} +
{projectActionsTarget?.memberProjects.map((member) => (
-
- -
-
- -

- {member.environmentLabel ?? "Current environment"} -

-
-

- {member.workspaceRoot} -

-
-
-
+
-
- - -
+ {projectActionsTarget.memberProjects.length > 1 ? ( +
+ +
+ ) : null}
))}
@@ -2727,8 +2728,26 @@ export default function SidebarV2() {
) : null} - - + + {projectActionsTarget?.memberProjects.length === 1 ? ( + + ) : null} + diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 866ed0c9b8cc0b4b1de7d91f0028c12a7d4b59f6..fa9a75dd3234ef542365d678b49b350dad4e9f8f 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -95,6 +95,7 @@ import { ComposerPrimaryActions } from "./ComposerPrimaryActions"; import { ComposerPendingApprovalPanel } from "./ComposerPendingApprovalPanel"; import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel"; import { ComposerPlanFollowUpBanner } from "./ComposerPlanFollowUpBanner"; +import { ComposerControl, ComposerControlIcon, ComposerSelectControl } from "./ComposerControl"; import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight"; import { searchSlashCommandItems } from "./composerSlashCommandSearch"; import { @@ -167,7 +168,7 @@ function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children: ); } import { Button } from "../ui/button"; -import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select"; +import { Select, SelectItem, SelectPopup, SelectValue } from "../ui/select"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { toastManager } from "../ui/toast"; import { @@ -303,15 +304,13 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop {props.interactionMode === "plan" ? ( - + ) : ( - + )} {props.interactionMode === "plan" ? "Plan" : "Build"} @@ -342,16 +341,9 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop onValueChange={(value) => props.onRuntimeModeChange(value!)} > - } + render={} > - + {runtimeModeOption.label} @@ -387,22 +379,21 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop } > - {props.planSidebarLabel} diff --git a/apps/web/src/components/chat/ComposerBannerStack.tsx b/apps/web/src/components/chat/ComposerBannerStack.tsx index 3f8f56f041a64342613b9d51efa54d72752c1743..699c9cfd9c4994358e6be0d83de49419e69407ef 100644 --- a/apps/web/src/components/chat/ComposerBannerStack.tsx +++ b/apps/web/src/components/chat/ComposerBannerStack.tsx @@ -93,7 +93,7 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro {showCollapsedStackCap ? (
{item.icon} diff --git a/apps/web/src/components/chat/ComposerCommandMenu.tsx b/apps/web/src/components/chat/ComposerCommandMenu.tsx index 9021b6b46091882dfb8fd4ebd154ee222d5277a7..73fc6348905670e89bd729cfb4bf2fd45b22abae 100644 --- a/apps/web/src/components/chat/ComposerCommandMenu.tsx +++ b/apps/web/src/components/chat/ComposerCommandMenu.tsx @@ -139,7 +139,10 @@ export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: { ); }} > -
+
{props.items.length > 0 ? ( {groups.map((group, groupIndex) => ( diff --git a/apps/web/src/components/chat/ComposerControl.tsx b/apps/web/src/components/chat/ComposerControl.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8eab75171c824d1246561e8ae0bbe5995d8a85eb --- /dev/null +++ b/apps/web/src/components/chat/ComposerControl.tsx @@ -0,0 +1,71 @@ +import type { ComponentProps } from "react"; +import { ChevronDownIcon, type LucideIcon } from "lucide-react"; + +import { cn } from "~/lib/utils"; +import { Button } from "../ui/button"; +import { SelectTrigger } from "../ui/select"; + +const composerControlClassName = + "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"; + +export function ComposerControl({ + className, + size = "sm", + variant = "ghost", + ...props +}: ComponentProps) { + return ( + diff --git a/apps/web/src/components/chat/ModelListRow.tsx b/apps/web/src/components/chat/ModelListRow.tsx index 2ec4be70994b6ac6c0ba12521fc8900c6c6c10db..e86435561a2a3fd9dff27e22d725daac93bb2e5a 100644 --- a/apps/web/src/components/chat/ModelListRow.tsx +++ b/apps/web/src/components/chat/ModelListRow.tsx @@ -50,7 +50,7 @@ export const ModelListRow = memo(function ModelListRow(props: { disabled={Boolean(props.disabledReason)} contentClassName="flex w-full items-center gap-3" className={cn( - "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]", + "group relative w-full !min-w-0 max-w-full cursor-pointer rounded-md px-2 py-2 transition-[background-color,box-shadow,color]", "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))]", props.disabledReason && "data-disabled:pointer-events-auto data-disabled:cursor-not-allowed data-disabled:hover:bg-transparent", diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index bbdbd8bd9d9647766d9b36e05f8ff683f965de31..d317ee6061c6bc9512d7bc8e5fde1e204db5ea6d 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -43,6 +43,10 @@ type ModelPickerItem = { const EMPTY_MODEL_JUMP_LABELS = new Map(); +function ModelListSeparator() { + return
; +} + // Split a `${instanceId}:${slug}` combobox key back into its pieces. Slugs // can contain colons (e.g. some vendor model ids), so we only split on the // first colon — anything after that is the slug. @@ -521,7 +525,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { return (
{/* Sidebar */} @@ -571,12 +575,12 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
{/* Search bar */} -
-
+
+
+ } value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} @@ -618,8 +622,8 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
{/* Model list */} -
- +
+ ref={modelListRef} data={filteredModelKeys} @@ -656,12 +660,14 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { estimatedItemSize={60} drawDistance={480} recycleItems + contentContainerClassName="pl-2 pr-px" + ItemSeparatorComponent={ModelListSeparator} onLayout={updateModelListScrollFades} onScroll={updateModelListScrollFades} className={cn( - "scrollbar-gutter-both h-full overflow-x-hidden overscroll-y-contain py-1.5 [--fade-size:1.5rem]", - showTopScrollFade && "mask-t-from-[calc(100%-var(--fade-size))]", - showBottomScrollFade && "mask-b-from-[calc(100%-var(--fade-size))]", + "model-picker-list h-full overflow-x-hidden overscroll-y-contain py-1.5 [--fade-size:1.5rem]", + showTopScrollFade && "model-picker-list-scroll-fade-top", + showBottomScrollFade && "model-picker-list-scroll-fade-bottom", )} /> diff --git a/apps/web/src/components/chat/ModelPickerSidebar.tsx b/apps/web/src/components/chat/ModelPickerSidebar.tsx index 36a608888b2e5b56c66f6e97c7fc9ed5c6591a1b..24ec66cd6142f2b398859e7f83a5ab7fc92bb211 100644 --- a/apps/web/src/components/chat/ModelPickerSidebar.tsx +++ b/apps/web/src/components/chat/ModelPickerSidebar.tsx @@ -78,34 +78,20 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { if (!content) { return; } - const selectedButton = Array.from( + const selectedItem = Array.from( content.querySelectorAll("[data-model-picker-provider]"), - ).find((button) => button.dataset.modelPickerProvider === props.selectedInstanceId); - if (!selectedButton) { + ).find((item) => item.dataset.modelPickerProvider === props.selectedInstanceId); + if (!selectedItem) { setSelectedIndicatorTop(null); return; } - const contentRect = content.getBoundingClientRect(); - const selectedButtonRect = selectedButton.getBoundingClientRect(); - setSelectedIndicatorTop( - selectedButtonRect.top - - contentRect.top + - content.scrollTop + - selectedButtonRect.height / 2 - - 10, - ); + setSelectedIndicatorTop(selectedItem.offsetTop + selectedItem.offsetHeight / 2 - 10); }, [props.instanceEntries, props.selectedInstanceId, showFavorites]); return ( -
+
-
+
{selectedIndicatorTop !== null ? (
-
+ <> +
handleSelect("favorites")} type="button" - data-model-picker-provider="favorites" aria-label="Favorites" > @@ -146,7 +131,8 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
-
+