diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index f666e692860..4c9e99e6de1 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -24,6 +24,10 @@ const clientSettings: ClientSettings = { sidebarProjectGroupingOverrides: { "environment-1:/tmp/project-a": "separate", }, + sidebarProjectColorizing: false, + sidebarProjectColorOverrides: { + "environment-1:/tmp/project-a": "indigo", + }, sidebarProjectSortOrder: "manual", sidebarThreadSortOrder: "created_at", sidebarThreadPreviewCount: 6, diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index e8c5bbe0b11..c7fc23f56a6 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -56,6 +56,7 @@ import { Link, useLocation, useNavigate, useParams, useRouter } from "@tanstack/ import { MAX_SIDEBAR_THREAD_PREVIEW_COUNT, MIN_SIDEBAR_THREAD_PREVIEW_COUNT, + type SidebarProjectColor, type SidebarProjectSortOrder, type SidebarThreadPreviewCount, type SidebarThreadSortOrder, @@ -64,7 +65,7 @@ import { usePrimaryEnvironmentId } from "../environments/primary"; import { isElectron } from "../env"; import { APP_STAGE_LABEL, APP_VERSION } from "../branding"; import { isTerminalFocused } from "../lib/terminalFocus"; -import { isMacPlatform, newCommandId } from "../lib/utils"; +import { cn, isMacPlatform, newCommandId } from "../lib/utils"; import { selectProjectByRef, selectProjectsAcrossEnvironments, @@ -125,6 +126,7 @@ import { Input } from "./ui/input"; import { Menu, MenuGroup, + MenuItem, MenuPopup, MenuRadioGroup, MenuRadioItem, @@ -197,6 +199,12 @@ import { type SidebarProjectSnapshot, } from "../sidebarProjectGrouping"; import { SidebarProviderUpdatePill } from "./sidebar/SidebarProviderUpdatePill"; +import { + buildSidebarProjectColorMap, + EMPTY_SIDEBAR_PROJECT_COLOR_MAP, + SIDEBAR_PROJECT_COLOR_PALETTE, + type SidebarProjectColorIdentity, +} from "../sidebarProjectColors"; const SIDEBAR_SORT_LABELS: Record = { updated_at: "Last user message", created_at: "Created at", @@ -246,6 +254,109 @@ function projectGroupingModeDescription(mode: SidebarProjectGroupingMode): strin } } +interface ProjectColorDotProps { + identity: SidebarProjectColorIdentity; + projectName: string; + onSelect: (color: SidebarProjectColor | null) => void; + /** + * Extra classes for the trigger itself — typically the absolute positioning + * applied by the parent. Putting positioning on the trigger directly (rather + * than a wrapping div) keeps this dot's box dimensions identical to the + * sibling new-thread button so they line up vertically. + */ + className?: string; +} + +/** + * Always-visible color dot rendered to the left of the per-project new-thread + * button when "Sidebar project colors" is enabled. Clicking opens a palette + * menu; selecting a swatch persists the override; selecting "Auto" clears it. + */ +const ProjectColorDot = memo(function ProjectColorDot({ + identity, + projectName, + onSelect, + className, +}: ProjectColorDotProps) { + return ( + + + { + // The wrapping project header treats unhandled pointerdown as + // the start of a row drag. Stop the event so opening the + // picker doesn't also begin a sortable drag. + event.stopPropagation(); + }} + onClick={(event) => { + event.stopPropagation(); + }} + /> + } + > + + + {identity.override + ? `Color: ${identity.palette.label}` + : `Color: ${identity.palette.label} (auto)`} + + + + +
+ Project color +
+ onSelect(null)} + className={cn( + "min-h-7 py-1 sm:text-xs", + identity.override === undefined && "font-medium", + )} + > + + + {SIDEBAR_PROJECT_COLOR_PALETTE.map((entry) => { + const isSelected = identity.override === entry.key; + return ( + onSelect(entry.key)} + className={cn("min-h-7 py-1 sm:text-xs", isSelected && "font-medium")} + > + + ); + })} +
+
+
+ ); +}); + function buildThreadJumpLabelMap(input: { keybindings: ReturnType; platform: string; @@ -763,6 +874,12 @@ interface SidebarProjectThreadListProps { openPrLink: (event: React.MouseEvent, prUrl: string) => void; expandThreadListForProject: (projectKey: string) => void; collapseThreadListForProject: (projectKey: string) => void; + /** + * When true, hides the vertical guide line on the left of the thread list. + * Used to declutter the sidebar when project tints provide their own + * grouping cue. + */ + hideThreadListBorder: boolean; } const SidebarProjectThreadList = memo(function SidebarProjectThreadList( @@ -802,6 +919,7 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList( openPrLink, expandThreadListForProject, collapseThreadListForProject, + hideThreadListBorder, } = props; const showMoreButtonRender = useMemo(() =>