From c7b85d3f5d90c6045d734b62c9253b3c0aced5b8 Mon Sep 17 00:00:00 2001 From: maria-rcks <254055478+maria-rcks@users.noreply.github.com> Date: Sun, 13 Sep 2026 04:24:55 +0000 Subject: [PATCH 01/11] feat(web): add optional compact sidebar rail --- apps/web/src/components/AppSidebarLayout.tsx | 9 +- apps/web/src/components/LegacySidebar.tsx | 77 ++++--- apps/web/src/components/Sidebar.tsx | 212 +++++++++++++++--- .../components/settings/SettingsPanels.tsx | 21 ++ .../settings/SettingsSidebarNav.tsx | 33 ++- .../src/components/settings/settingsSearch.ts | 6 + .../src/components/sidebar/SidebarChrome.tsx | 14 +- .../sidebar/SidebarThreadHeader.tsx | 31 ++- .../components/sidebar/SidebarUpdatePill.tsx | 2 +- apps/web/src/hooks/useSettings.ts | 7 + packages/contracts/src/settings.ts | 2 + 11 files changed, 344 insertions(+), 70 deletions(-) diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx index 6769586f7fa8..6497df68c8bd 100644 --- a/apps/web/src/components/AppSidebarLayout.tsx +++ b/apps/web/src/components/AppSidebarLayout.tsx @@ -14,7 +14,11 @@ import { getLocalStorageItem, removeLocalStorageItem } from "../hooks/useLocalSt import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; import { cn, isMacPlatform } from "../lib/utils"; import { primaryServerKeybindingsAtom } from "../state/server"; -import { useEnvironmentIdentificationMode, useLegacySidebarEnabled } from "../hooks/useSettings"; +import { + useCompactSidebarEnabled, + useEnvironmentIdentificationMode, + useLegacySidebarEnabled, +} from "../hooks/useSettings"; import { PanelAnimationSuppressionProvider, usePanelAnimationSettings, @@ -144,6 +148,7 @@ function ProjectProjectionRetention() { export function AppSidebarLayout({ children }: { children: ReactNode }) { const navigate = useNavigate(); const legacySidebarEnabled = useLegacySidebarEnabled(); + const compactSidebarEnabled = useCompactSidebarEnabled(); const { active: panelAnimationsActive, durationMs: panelAnimationDurationMs } = usePanelAnimationSettings(); // Settings routes show the settings nav in place of whichever thread @@ -229,7 +234,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { s.queuePendingFileDrop); const clearPendingFileDrop = useSidebarPendingFileDropStore((s) => s.clearPendingFileDrop); - const { isMobile, setOpenMobile } = useSidebar(); + const { isMobile, setOpenMobile, state, setOpen } = useSidebar(); + const compactSidebarEnabled = useCompactSidebarEnabled(); + const isCompact = compactSidebarEnabled && !isMobile && state === "collapsed"; const markThreadUnread = useUiStateStore((state) => state.markThreadUnread); const setProjectExpanded = useUiStateStore((state) => state.setProjectExpanded); const toggleThreadSelection = useThreadSelectionStore((state) => state.toggleThread); @@ -1445,10 +1451,13 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec if (useThreadSelectionStore.getState().hasSelection()) { clearSelection(); } - setProjectExpanded(projectPreferenceKeys, !projectExpanded); + setProjectExpanded(projectPreferenceKeys, isCompact || !projectExpanded); + if (isCompact) setOpen(true); }, [ clearSelection, + isCompact, + setOpen, dragInProgressRef, projectExpanded, projectPreferenceKeys, @@ -1465,9 +1474,17 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec if (dragInProgressRef.current) { return; } - setProjectExpanded(projectPreferenceKeys, !projectExpanded); + setProjectExpanded(projectPreferenceKeys, isCompact || !projectExpanded); + if (isCompact) setOpen(true); }, - [dragInProgressRef, projectExpanded, projectPreferenceKeys, setProjectExpanded], + [ + dragInProgressRef, + isCompact, + projectExpanded, + projectPreferenceKeys, + setOpen, + setProjectExpanded, + ], ); const handleProjectButtonPointerDownCapture = useCallback( @@ -2359,8 +2376,10 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec <>
- {!projectExpanded && projectStatus ? ( + {isCompact ? null : !projectExpanded && projectStatus ? ( - + {project.displayName} @@ -2415,7 +2434,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec {/* Environment badge – visible by default, crossfades with the "new thread" button on hover using the same pointer-events + opacity pattern as the thread row archive/timestamp swap. */} - {project.environmentPresence === "remote-only" && ( + {!isCompact && project.environmentPresence === "remote-only" && ( +
+ + + } + > + {content} + + {props.label} + ); } @@ -704,6 +728,9 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: { onDiscard: (draftId: DraftId) => void; }) { const { composer, draftId, onDiscard, onNavigate, session } = props; + const compactEnabled = useCompactSidebarEnabled(); + const { state, isMobile } = useSidebar(); + const compact = compactEnabled && state === "collapsed" && !isMobile; const promptPreview = replaceComposerContextReferences(composer.prompt, (occurrence) => occurrence.label) .trim() @@ -742,6 +769,34 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: { }, [draftId, onDiscard], ); + if (compact) { + return ( +
  • + + + } + > + + + +
    {props.projectDisplayName}
    +
    {preview}
    +
    +
    +
  • + ); + } return (
  • scopeThreadRef(thread.environmentId, thread.id), [thread.environmentId, thread.id], @@ -1208,6 +1266,16 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { branchMismatch={branchMismatch} terminalStatus={terminalStatus} terminalProcessCount={terminalProcessCount} + compactStatus={ + compact + ? (topStatus?.label ?? + (variantAction === "unsnooze" + ? "Snoozed" + : variantAction === "unsettle" + ? "Settled" + : "Ready")) + : undefined + } /> ); @@ -1557,6 +1625,68 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { ) ) : null; + if (compact) { + return ( +
  • + + + } + > + {props.project ? ( + + ) : driverKind ? ( + + ) : ( + + )} + {topStatus ? ( + + ) : hasUnsentDraft ? ( + + ) : null} + {props.jumpLabel ? : null} + + {detailsTooltip} + +
  • + ); + } + if (variant === "slim") { return (
  • void; }) { const { thread } = props; + const compactEnabled = useCompactSidebarEnabled(); + const { state, isMobile } = useSidebar(); + const compact = compactEnabled && state === "collapsed" && !isMobile; const threadRef = useMemo( () => scopeThreadRef(thread.environmentId, thread.id), [thread.environmentId, thread.id], @@ -2074,6 +2207,7 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: { onClick={props.onSelect} className={cn( "flex h-9 w-full cursor-pointer items-center gap-2.5 rounded-md px-2.5 text-left text-sm outline-none", + compact && "justify-center px-0", props.isHighlighted || props.isRouteActive ? "bg-sidebar-row-active text-sidebar-foreground" : "text-sidebar-muted-foreground/75 hover:bg-sidebar-row-hover hover:text-sidebar-foreground", @@ -2085,9 +2219,15 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: { > {props.project ? ( + ) : compact ? ( + ) : null} - {thread.title} - + {thread.title} + {threadTimeLabel(thread)} @@ -2115,7 +2255,9 @@ export default function Sidebar() { const projectOrder = useUiStateStore((store) => store.projectOrder); const threads = useThreadShells(); const router = useRouter(); - const { isMobile, setOpenMobile } = useSidebar(); + const { isMobile, setOpenMobile, setOpen, state: sidebarState } = useSidebar(); + const compactEnabled = useCompactSidebarEnabled(); + const compact = compactEnabled && sidebarState === "collapsed" && !isMobile; const keybindings = useAtomValue(primaryServerKeybindingsAtom); const confirmThreadDelete = useClientSettings((s) => s.confirmThreadDelete); const confirmThreadArchive = useClientSettings((s) => s.confirmThreadArchive); @@ -2922,10 +3064,14 @@ export default function Sidebar() { const [renamingThreadKey, setRenamingThreadKey] = useState(null); const [renamingTitle, setRenamingTitle] = useState(""); - const startThreadRename = useCallback((threadRef: ScopedThreadRef, title: string) => { - setRenamingThreadKey(scopedThreadKey(threadRef)); - setRenamingTitle(title); - }, []); + const startThreadRename = useCallback( + (threadRef: ScopedThreadRef, title: string) => { + if (compact) setOpen(true); + setRenamingThreadKey(scopedThreadKey(threadRef)); + setRenamingTitle(title); + }, + [compact, setOpen], + ); const cancelThreadRename = useCallback(() => setRenamingThreadKey(null), []); const commitThreadRename = useCallback( (threadRef: ScopedThreadRef, title: string, originalTitle: string) => { @@ -4381,8 +4527,12 @@ export default function Sidebar() { // popup opens under the field, is at least as wide as it, // and grows to fit project names up to a cap, past which // the rows truncate. - anchor={headerSearchRef} - className="max-w-[min(18rem,var(--available-width))] overflow-hidden" + anchor={compact ? undefined : headerSearchRef} + side={compact ? "right" : "bottom"} + className={cn( + "max-w-[min(18rem,var(--available-width))] overflow-hidden", + compact && "min-w-56", + )} > No threads found

    @@ -4836,20 +4989,29 @@ export default function Sidebar() { snoozedThreads.length + settledThreads.length === 0 ? ( -
    +
    {projects.length === 0 ? ( <> - No projects yet + No projects yet - ) : scopedProjectGroup ? ( + ) : compact ? null : scopedProjectGroup ? ( `No threads in ${scopedProjectGroup.displayName} yet` ) : ( "No threads yet" diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index c9700496f420..d48f1781172e 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -501,6 +501,9 @@ export function useSettingsRestore(onRestored?: () => void) { ...(theme !== "system" ? ["Theme"] : []), ...(!followSystem ? ["Follow system"] : []), ...(themeHalves !== null ? ["Theme mix"] : []), + ...(settings.compactSidebarEnabled !== DEFAULT_UNIFIED_SETTINGS.compactSidebarEnabled + ? ["Compact sidebar"] + : []), ...(settings.appearanceContrast !== DEFAULT_UNIFIED_SETTINGS.appearanceContrast ? ["Contrast"] : []), @@ -605,6 +608,7 @@ export function useSettingsRestore(onRestored?: () => void) { settings.browserLinkTarget, settings.browserAutoShowFloatingPreview, settings.appearanceContrast, + settings.compactSidebarEnabled, settings.diffColorScheme, settings.enableAgentBrowserAccess, settings.confirmQuit, @@ -712,6 +716,7 @@ export function useSettingsRestore(onRestored?: () => void) { } updateSettings({ appearanceContrast: DEFAULT_UNIFIED_SETTINGS.appearanceContrast, + compactSidebarEnabled: DEFAULT_UNIFIED_SETTINGS.compactSidebarEnabled, diffColorScheme: DEFAULT_UNIFIED_SETTINGS.diffColorScheme, timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat, notificationMode: DEFAULT_UNIFIED_SETTINGS.notificationMode, @@ -1357,6 +1362,22 @@ export function AppearanceSettingsPanel() { + + + + updateSettings({ compactSidebarEnabled: Boolean(checked) }) + } + aria-label="Compact sidebar" + /> + } + /> + ); } diff --git a/apps/web/src/components/settings/SettingsSidebarNav.tsx b/apps/web/src/components/settings/SettingsSidebarNav.tsx index c29068934efa..c206174a397a 100644 --- a/apps/web/src/components/settings/SettingsSidebarNav.tsx +++ b/apps/web/src/components/settings/SettingsSidebarNav.tsx @@ -24,6 +24,7 @@ import { XIcon, } from "lucide-react"; import { useLocation, useNavigate } from "@tanstack/react-router"; +import { useCompactSidebarEnabled } from "../../hooks/useSettings"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; @@ -111,12 +112,13 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { (item) => item.to !== "/settings/projects" || isSettingsOverviewVisible(scopeSearch), ); const { isMobile, setOpenMobile, open, setOpen } = useSidebar(); + const compactSidebarEnabled = useCompactSidebarEnabled(); const searchInputRef = useRef(null); const [query, setQuery] = useState(""); const [activeResultIndex, setActiveResultIndex] = useState(0); const searchableItems = useAvailableSettingsSearchItems(); const results = useMemo(() => searchSettings(query, searchableItems), [query, searchableItems]); - const isSearching = query.trim().length > 0; + const isSearching = query.trim().length > 0 && !(compactSidebarEnabled && !isMobile && !open); const hasResults = results.length > 0; useEffect(() => { @@ -233,7 +235,18 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { <> -
    + { + setOpen(true); + requestAnimationFrame(() => searchInputRef.current?.focus()); + }} + > + + +
    handleSectionClick(item.to)} > - {item.label} + + {item.label} + ); @@ -343,10 +360,12 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { - - - -
    +
    + + + +
    +
    diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts index 9a8af52b90f5..1bef4848e764 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -159,6 +159,12 @@ export const SETTINGS_SEARCH_ITEMS = [ title: "Panel animations", to: "/settings/appearance", }, + { + id: "compact-sidebar", + title: "Compact sidebar", + to: "/settings/appearance", + searchTerms: ["advanced collapsed icons rail hover navigation"], + }, { id: "environment-identification", title: "Environment identification", diff --git a/apps/web/src/components/sidebar/SidebarChrome.tsx b/apps/web/src/components/sidebar/SidebarChrome.tsx index afbbf7671dfc..2f65cf8c3697 100644 --- a/apps/web/src/components/sidebar/SidebarChrome.tsx +++ b/apps/web/src/components/sidebar/SidebarChrome.tsx @@ -86,7 +86,7 @@ function SidebarBrand({ onBackdrop }: { onBackdrop: boolean }) { + {currentFooterPage ? ( - + - Back + Back ) : ( @@ -224,8 +224,10 @@ export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() { export const SidebarChromeFooter = memo(function SidebarChromeFooter() { return ( - - +
    + + +
    ); diff --git a/apps/web/src/components/sidebar/SidebarThreadHeader.tsx b/apps/web/src/components/sidebar/SidebarThreadHeader.tsx index 878235615b39..d0718d9856f8 100644 --- a/apps/web/src/components/sidebar/SidebarThreadHeader.tsx +++ b/apps/web/src/components/sidebar/SidebarThreadHeader.tsx @@ -20,9 +20,10 @@ import { } from "react"; import { cn } from "~/lib/utils"; +import { useCompactSidebarEnabled } from "../../hooks/useSettings"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; -import { SidebarMenuButton } from "../ui/sidebar"; +import { SidebarMenuButton, useSidebar } from "../ui/sidebar"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; export interface SidebarThreadHeaderProps { @@ -69,6 +70,9 @@ export function SidebarThreadHeader({ activeSearchResultIndex, onClearSearch, }: SidebarThreadHeaderProps) { + const compactEnabled = useCompactSidebarEnabled(); + const { state, isMobile, setOpen } = useSidebar(); + const compact = compactEnabled && state === "collapsed" && !isMobile; const resultsVisible = isSearching && searchResultCount > 0; // Results shrink as the query narrows, so the active index can outrun the // list; pointing aria-activedescendant at a removed option strands the @@ -79,10 +83,24 @@ export function SidebarThreadHeader({ : "New thread"; return ( -
    +
    + {compact ? ( + { + setOpen(true); + requestAnimationFrame(() => searchInputRef.current?.focus()); + }} + > + + + ) : null}
    {/* Segmented well: the icons read as one control instead of three loose buttons competing with the search field beside them. */} -
    +
    {hasProjects ? ( <> {projectScope} diff --git a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx index a94b7801ecfd..8c04eec6fe7d 100644 --- a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx +++ b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx @@ -348,7 +348,7 @@ function SidebarUpdateControl() { ); return ( - + { diff --git a/apps/web/src/hooks/useSettings.ts b/apps/web/src/hooks/useSettings.ts index 194cc36c55f4..fa4b8bc8fd31 100644 --- a/apps/web/src/hooks/useSettings.ts +++ b/apps/web/src/hooks/useSettings.ts @@ -379,6 +379,13 @@ export function useLegacySidebarEnabled(): boolean { return settingsHydrated && legacySidebarEnabled; } +/** Keep the default collapsed sidebar until persisted client settings hydrate. */ +export function useCompactSidebarEnabled(): boolean { + const settingsHydrated = useClientSettingsHydrated(); + const compactSidebarEnabled = useClientSettingsValue().compactSidebarEnabled; + return settingsHydrated && compactSidebarEnabled; +} + /** Read current settings for one environment, merged with client-local preferences. */ export function useEnvironmentSettings( environmentId: EnvironmentId, diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 004a53b320e8..826440556d2c 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -436,6 +436,7 @@ export const ClientSettingsSchema = Schema.Struct({ // old keys, so everyone, including prior beta opt-outs, resets to the new // default sidebar. legacySidebarEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), + compactSidebarEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), sidebarProjectGroupingMode: SidebarProjectGroupingMode.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE)), ), @@ -1490,6 +1491,7 @@ export const ClientSettingsPatch = Schema.Struct({ proactivePanelsEnabled: Schema.optionalKey(Schema.Boolean), showSkillsInSlashMenu: Schema.optionalKey(Schema.Boolean), legacySidebarEnabled: Schema.optionalKey(Schema.Boolean), + compactSidebarEnabled: Schema.optionalKey(Schema.Boolean), sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode), sidebarProjectGroupingOverrides: Schema.optionalKey( Schema.Record(TrimmedNonEmptyString, SidebarProjectGroupingMode), From 8b9b82145b69361744b435a82bdc4d59c1b5a1ee Mon Sep 17 00:00:00 2001 From: maria-rcks <254055478+maria-rcks@users.noreply.github.com> Date: Sun, 13 Sep 2026 04:39:45 +0000 Subject: [PATCH 02/11] fix(web): preserve compact sidebar history and drag controls --- apps/web/src/components/Sidebar.tsx | 32 +++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 6dd26b9fb7cf..7be5533fc530 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -1631,7 +1631,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { data-thread-item {...sortableRootProps} {...(fileDropHandlers ?? {})} - className="list-none py-0.5" + className={cn("list-none py-0.5", sortable?.isDragging && "relative z-20")} > 0 ? (
  • - + + + } + > + + + Show {Math.min(hiddenSettledCount, SETTLED_TAIL_PAGE_COUNT)} more + + + + Show {Math.min(hiddenSettledCount, SETTLED_TAIL_PAGE_COUNT)} more + +
  • ) : null} From 1bef2d6050b83fe0ced5fe33bb8b058d7a91570b Mon Sep 17 00:00:00 2001 From: maria-rcks <254055478+maria-rcks@users.noreply.github.com> Date: Sun, 13 Sep 2026 04:47:17 +0000 Subject: [PATCH 03/11] fix(web): show compact sidebar drop action feedback --- apps/web/src/components/Sidebar.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 7be5533fc530..13e6a9fec0cc 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -1322,6 +1322,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { [isRenaming, onStartRename, thread.title, threadRef], ); const [isFileDragOver, setIsFileDragOver] = useState(false); + const [compactTooltipOpen, setCompactTooltipOpen] = useState(false); const fileDropHandlers = useMemo( () => onFileDropThreads @@ -1633,7 +1634,10 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { {...(fileDropHandlers ?? {})} className={cn("list-none py-0.5", sortable?.isDragging && "relative z-20")} > - + : null} - {detailsTooltip} + {sortable?.isDragging ? ( + {dragDestination} + ) : ( + detailsTooltip + )} ); From ceb21331d6e178722560153cf5cfcbb7fab47afa Mon Sep 17 00:00:00 2001 From: maria-rcks <254055478+maria-rcks@users.noreply.github.com> Date: Sun, 13 Sep 2026 04:50:59 +0000 Subject: [PATCH 04/11] fix(web): preserve tooltips across sidebar modes --- apps/web/src/components/Sidebar.tsx | 48 +++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 13e6a9fec0cc..64fab8f7b82f 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -587,7 +587,18 @@ function SidebarSectionPlaceholder(props: { props.isDropTarget && "border-primary/40 bg-primary/5 text-primary", )} > - {props.label} + {props.label} + {props.marker === "settled-placeholder" ? ( + + ) : ( + + )}
    ) : null} @@ -611,19 +622,30 @@ function SidebarDragBoundary(props: { className="pointer-events-none relative mx-0.5 -mb-px h-0" > {props.visible ? ( -
    +
    - {props.label} + {props.label} + {props.marker === "pinned-header" ? ( + + ) : ( + + )} @@ -1322,7 +1344,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { [isRenaming, onStartRename, thread.title, threadRef], ); const [isFileDragOver, setIsFileDragOver] = useState(false); - const [compactTooltipOpen, setCompactTooltipOpen] = useState(false); + const [tooltipOpen, setTooltipOpen] = useState(false); const fileDropHandlers = useMemo( () => onFileDropThreads @@ -1635,8 +1657,8 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { className={cn("list-none py-0.5", sortable?.isDragging && "relative z-20")} > - + - + Date: Sun, 13 Sep 2026 05:20:32 +0000 Subject: [PATCH 05/11] fix(web): remove duplicate compact sidebar header inset --- apps/web/src/workspaceTitlebar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/workspaceTitlebar.ts b/apps/web/src/workspaceTitlebar.ts index b481221e63aa..aed95897cc55 100644 --- a/apps/web/src/workspaceTitlebar.ts +++ b/apps/web/src/workspaceTitlebar.ts @@ -1,2 +1,2 @@ export const COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS = - "[[data-sidebar-state=collapsed]_&]:pl-[var(--workspace-titlebar-content-left)]"; + "[[data-sidebar-state=collapsed]_&]:pl-[var(--workspace-titlebar-content-left)] [[data-sidebar-state=collapsed]:has([data-side=left][data-collapsible=icon])_&]:pl-[max(calc(env(safe-area-inset-left)+1.25rem),calc(var(--workspace-titlebar-content-left)-var(--sidebar-width-icon)))]"; From cbdcb003a2a6d544152cddf0e2f60f0fb01867c3 Mon Sep 17 00:00:00 2001 From: maria-rcks <254055478+maria-rcks@users.noreply.github.com> Date: Sun, 13 Sep 2026 06:04:55 +0000 Subject: [PATCH 06/11] feat(web): add interactive compact sidebar setting preview --- .../settings/CompactSidebarPreview.tsx | 53 +++++++++++++++++++ .../components/settings/SettingsPanels.tsx | 28 ++++++---- .../src/components/settings/settingsSearch.ts | 2 +- 3 files changed, 71 insertions(+), 12 deletions(-) create mode 100644 apps/web/src/components/settings/CompactSidebarPreview.tsx diff --git a/apps/web/src/components/settings/CompactSidebarPreview.tsx b/apps/web/src/components/settings/CompactSidebarPreview.tsx new file mode 100644 index 000000000000..49fb0754e015 --- /dev/null +++ b/apps/web/src/components/settings/CompactSidebarPreview.tsx @@ -0,0 +1,53 @@ +import { useState } from "react"; + +import { cn } from "~/lib/utils"; + +export function CompactSidebarPreview() { + const [collapsed, setCollapsed] = useState(false); + + return ( + + ); +} diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index d48f1781172e..c9174e07e28c 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -160,6 +160,7 @@ import { import { searchableSetting } from "./settingsSearch"; import { ProjectFavicon } from "../ProjectFavicon"; import { PanelAnimationsPreview } from "./PanelAnimationsPreview"; +import { CompactSidebarPreview } from "./CompactSidebarPreview"; const ENVIRONMENT_IDENTIFICATION_LABELS: Record = { artwork: "Artwork", @@ -1361,23 +1362,28 @@ export function AppearanceSettingsPanel() { /> - - - + - updateSettings({ compactSidebarEnabled: Boolean(checked) }) - } - aria-label="Compact sidebar" - /> +
    + +
    + + updateSettings({ compactSidebarEnabled: Boolean(checked) }) + } + aria-label="Compact sidebar" + /> +
    +
    } />
    + + ); } diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts index 1bef4848e764..be62ad6b25b5 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -163,7 +163,7 @@ export const SETTINGS_SEARCH_ITEMS = [ id: "compact-sidebar", title: "Compact sidebar", to: "/settings/appearance", - searchTerms: ["advanced collapsed icons rail hover navigation"], + searchTerms: ["collapsed icons rail hover navigation preview"], }, { id: "environment-identification", From 0c0226576f276828ec42957c2579c1d83e402456 Mon Sep 17 00:00:00 2001 From: maria-rcks <254055478+maria-rcks@users.noreply.github.com> Date: Sun, 13 Sep 2026 06:32:08 +0000 Subject: [PATCH 07/11] fix(web): place compact sidebar preview beside toggle --- apps/web/src/components/settings/SettingsPanels.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index c9174e07e28c..efefc79c32cd 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -1367,7 +1367,7 @@ export function AppearanceSettingsPanel() { {...searchableSetting("compact-sidebar")} description="Keep an icon rail when the sidebar is collapsed. Click the preview to try it." control={ -
    +
    Date: Sun, 13 Sep 2026 07:40:08 +0000 Subject: [PATCH 08/11] fix(web): tighten compact rail and dock snoozed threads --- apps/web/src/components/Sidebar.tsx | 141 +++++++++++++++--- .../sidebar/SidebarThreadHeader.tsx | 99 +++++++----- apps/web/src/components/ui/sidebar.tsx | 3 + 3 files changed, 183 insertions(+), 60 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 64fab8f7b82f..a5420ffb8a36 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -6,6 +6,7 @@ import { replaceComposerContextReferences } from "@t3tools/shared/composerContex import * as Schema from "effect/Schema"; import { DndContext, + DragOverlay, useSensor, useSensors, type DragEndEvent, @@ -73,6 +74,7 @@ import { type MouseEvent as ReactMouseEvent, type ReactNode, } from "react"; +import { createPortal } from "react-dom"; import { useParams, useRouter } from "@tanstack/react-router"; import { useRightPanelStore } from "../rightPanelStore"; @@ -506,7 +508,7 @@ function SnoozePopoverButton(props: { type SortableThreadRowBag = Pick< ReturnType, "listeners" | "setNodeRef" | "transform" | "transition" | "isDragging" ->; +> & { hidden?: boolean }; function SortableThreadRow(props: { id: string; @@ -793,7 +795,7 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: { ); if (compact) { return ( -
  • +
  • @@ -1507,7 +1509,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { // A zero-height boundary also makes dnd-kit scale the source to // zero. Only projected peers use scaleY as a visibility sentinel. visibility: - !sortable.isDragging && sortable.transform?.scaleY === 0 + sortable.hidden || (!sortable.isDragging && sortable.transform?.scaleY === 0) ? ("hidden" as const) : undefined, }, @@ -1654,10 +1656,12 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { data-thread-item {...sortableRootProps} {...(fileDropHandlers ?? {})} - className={cn("list-none py-0.5", sortable?.isDragging && "relative z-20")} + className={cn("list-none", sortable?.isDragging && "relative z-20")} > (null); const keybindings = useAtomValue(primaryServerKeybindingsAtom); const confirmThreadDelete = useClientSettings((s) => s.confirmThreadDelete); const confirmThreadArchive = useClientSettings((s) => s.confirmThreadArchive); @@ -2867,6 +2872,7 @@ export default function Sidebar() { [setSettledShelfExpanded], ); const renderedSettledThreads = useMemo(() => { + if (compact) return EMPTY_THREADS; if (settledShelfExpanded) return visibleSettledThreads; if (routeThreadKey === null) return EMPTY_THREADS; const routeThread = visibleSettledThreads.find( @@ -2874,7 +2880,7 @@ export default function Sidebar() { scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)) === routeThreadKey, ); return routeThread === undefined ? EMPTY_THREADS : [routeThread]; - }, [routeThreadKey, settledShelfExpanded, visibleSettledThreads]); + }, [compact, routeThreadKey, settledShelfExpanded, visibleSettledThreads]); // The snoozed shelf is collapsed by default: out of the way, never gone. // Collapsed threads don't render (and so don't participate in jump @@ -3271,8 +3277,18 @@ export default function Sidebar() { const threadListRef = useRef(null); const dragLabelOffsetRef = useRef(0); const restrictBelowPins = useCallback( - (args) => restrictBelowSidebarLabel(args, dragLabelOffsetRef.current), - [], + (args) => + restrictBelowSidebarLabel( + { + ...args, + // The fixed snoozed shelf shares the main list's drag boundary. + containerNodeRect: compact + ? (threadListRef.current?.getBoundingClientRect() ?? args.containerNodeRect) + : args.containerNodeRect, + }, + dragLabelOffsetRef.current, + ), + [compact], ); const listMotionRef = useRef | null>(null); const attachListMotionRef = useCallback((node: HTMLUListElement | null) => { @@ -3494,7 +3510,7 @@ export default function Sidebar() { pinnedThreads.length + activeThreads.length + snoozedThreads.length + - settledThreads.length === + (compact ? 0 : settledThreads.length) === 0 ) { return []; @@ -3510,13 +3526,15 @@ export default function Sidebar() { items.push({ kind: "marker", marker: "snoozed-header" }); items.push(...rowsOf(visibleSnoozedThreads, "snoozed")); } - items.push({ kind: "marker", marker: "settled-header" }); - const settledRows = rowsOf(renderedSettledThreads, "settled"); - items.push({ kind: "marker", marker: "settled-placeholder" }); - items.push(...settledRows); + if (!compact) { + items.push({ kind: "marker", marker: "settled-header" }); + items.push({ kind: "marker", marker: "settled-placeholder" }); + items.push(...rowsOf(renderedSettledThreads, "settled")); + } return items; }, [ activeThreads, + compact, pinnedThreads, renderedSettledThreads, settledThreads.length, @@ -3602,6 +3620,25 @@ export default function Sidebar() { snoozedThreads.length, ], ); + const draggingCompactSnoozed = compact && dragState?.activeSection === "snoozed"; + const compactSnoozedDragThread = + draggingCompactSnoozed && dragState ? threadByKey.get(dragState.activeKey) : undefined; + const compactSidebarSortingStrategy = useCallback( + (args) => { + const item = sidebarListItems[args.index]; + // Footer rows stay anchored while the main list previews a reorder. + if ( + draggingCompactSnoozed || + (item?.kind === "thread" + ? item.section === "snoozed" + : item?.marker === "snoozed-header") + ) { + return null; + } + return sidebarSortingStrategy(args); + }, + [draggingCompactSnoozed, sidebarListItems, sidebarSortingStrategy], + ); // Hidden and filtered threads keep their keys. Reserve those slots without // including the rows in the visible drop order or writing to them. const { pinnedKeysById, activeKeysById } = useMemo( @@ -4506,6 +4543,15 @@ export default function Sidebar() { 0 ? ( +
      + ) : null + } fixedHeader={ // Lifted above the stage backdrop, whose fade bleeds below the // header and would otherwise paint across the search row's outline. @@ -4747,14 +4793,17 @@ export default function Sidebar() { modifiers={[ restrictToVerticalAxis, restrictBelowPins, - restrictToFirstScrollableAncestor, + ...(compact ? [] : [restrictToFirstScrollableAncestor]), ]} onDragStart={handleThreadDragStart} onDragOver={handleThreadDragOver} onDragEnd={handleThreadDragEnd} > - +
        - {(bag) => renderThreadRowInner(thread, section, bag)} + {(bag) => + renderThreadRowInner( + thread, + section, + draggingCompactSnoozed && bag.isDragging + ? { ...bag, hidden: true } + : bag, + ) + } ); }; const from = dragState?.activeSection ?? null; + const snoozedItems: ReactNode[] = []; const items: ReactNode[] = [ , ]; for (const item of sidebarListItems) { + const destination = + compact && + (item.kind === "thread" + ? item.section === "snoozed" + : item.marker === "snoozed-header") + ? snoozedItems + : items; if (item.kind === "thread") { - items.push(renderThreadRow(threadByKey.get(item.key)!, item.section)); + destination.push( + renderThreadRow(threadByKey.get(item.key)!, item.section), + ); continue; } switch (item.marker) { @@ -4946,7 +5013,7 @@ export default function Sidebar() { ); break; case "snoozed-header": - items.push( + destination.push( + {compactSnoozedDragThread ? ( +
          + {renderThreadRowInner( + compactSnoozedDragThread, + "snoozed", + { + isDragging: true, + listeners: undefined, + setNodeRef: () => {}, + transform: null, + transition: undefined, + }, + )} +
        + ) : null} + , + document.body, + "compact-snoozed-drag", + ) + : null, + ]; })()} - {settledShelfExpanded && hiddenSettledCount > 0 ? ( + {!compact && settledShelfExpanded && hiddenSettledCount > 0 ? (
      • 0; // Results shrink as the query narrows, so the active index can outrun the // list; pointing aria-activedescendant at a removed option strands the @@ -82,12 +85,13 @@ export function SidebarThreadHeader({ ? `New thread (${newThreadShortcutLabel})` : "New thread"; - return ( -
        + const actions = ( +
        {compact ? ( { + setActionsOpen(false); setOpen(true); requestAnimationFrame(() => searchInputRef.current?.focus()); }} @@ -95,6 +99,48 @@ export function SidebarThreadHeader({ ) : null} + {hasProjects ? ( + <> + {projectScope} + { + setActionsOpen(false); + onNewProject(); + }} + > + + + + ) : null} + + {newThreadLabel} + + New thread in current project: Shift+click + {newThreadInProjectShortcutLabel ? ` (${newThreadInProjectShortcutLabel})` : ""} + + + ) : ( + newThreadLabel + ) + } + disabled={newThreadDisabled} + onClick={(event) => { + setActionsOpen(false); + onNewThread(event); + }} + > + + +
        + ); + + return ( +
        {/* Segmented well: the icons read as one control instead of three loose buttons competing with the search field beside them. */} -
        - {hasProjects ? ( - <> - {projectScope} - - - - - ) : null} - - {newThreadLabel} - - New thread in current project: Shift+click - {newThreadInProjectShortcutLabel ? ` (${newThreadInProjectShortcutLabel})` : ""} - - - ) : ( - newThreadLabel - ) - } - disabled={newThreadDisabled} - onClick={onNewThread} - > - - -
        + {compact ? ( + + }> + + + + {actions} + + + ) : ( + actions + )}
        ); } diff --git a/apps/web/src/components/ui/sidebar.tsx b/apps/web/src/components/ui/sidebar.tsx index a7abcb3bb550..c8938497cc04 100644 --- a/apps/web/src/components/ui/sidebar.tsx +++ b/apps/web/src/components/ui/sidebar.tsx @@ -690,9 +690,11 @@ function SidebarSeparator({ className, ...props }: React.ComponentProps & { fixedHeader?: React.ReactNode; + fixedFooter?: React.ReactNode; }) { return ( <> @@ -716,6 +718,7 @@ function SidebarContent({ {...props} /> + {fixedFooter ?
        {fixedFooter}
        : null} ); } From f752944d906d4201b8f272fbe11646db1e3fb030 Mon Sep 17 00:00:00 2001 From: maria-rcks <254055478+maria-rcks@users.noreply.github.com> Date: Sun, 13 Sep 2026 07:49:56 +0000 Subject: [PATCH 09/11] fix(web): preserve compact drag geometry --- apps/web/src/components/Sidebar.drag.ts | 14 ++++++--- apps/web/src/components/Sidebar.tsx | 42 ++++++++++++------------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/apps/web/src/components/Sidebar.drag.ts b/apps/web/src/components/Sidebar.drag.ts index 0aef9dec14b1..c64de1c27b75 100644 --- a/apps/web/src/components/Sidebar.drag.ts +++ b/apps/web/src/components/Sidebar.drag.ts @@ -103,6 +103,7 @@ export function createSidebarSortingStrategy(input: { snoozedThreadCount?: number; cardHeight?: number; slimHeight?: number; + compact?: boolean; /** Space each pinned boundary opens for its label while dragging. The * markers stay zero height at rest, so nothing is reserved until pickup. */ boundaryLabelHeight?: number; @@ -140,11 +141,14 @@ export function createSidebarSortingStrategy(input: { else slimHeight ??= rects[index]?.height; if (item.key !== active.key) groups[item.section].push(item); } - // Cards are 4.875rem + 0.25rem padding; slim rows/placeholders are h-9. - const scale = - slimHeight !== undefined ? slimHeight / 36 : (headerScale ?? (cardHeight ?? 82) / 82); - cardHeight ??= 82 * scale; - slimHeight ??= 36 * scale; + // Compact icons use h-7; expanded cards include their vertical padding. + const scale = input.compact + ? (cardHeight ?? slimHeight ?? 28) / 28 + : slimHeight !== undefined + ? slimHeight / 36 + : (headerScale ?? (cardHeight ?? 82) / 82); + cardHeight ??= (input.compact ? 28 : 82) * scale; + slimHeight ??= (input.compact ? 28 : 36) * scale; const labelHeight = (input.boundaryLabelHeight ?? 0) * scale; const group = groups[target.section]; const order = diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index a5420ffb8a36..0cafc3f3585b 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -3604,6 +3604,7 @@ export default function Sidebar() { () => createSidebarSortingStrategy({ items: sidebarListItems, + compact, boundaryLabelHeight: SIDEBAR_DRAG_LABEL_HEIGHT, settledOrder: draggedSettledOrder, settledExpanded: settledShelfExpanded, @@ -3612,6 +3613,7 @@ export default function Sidebar() { snoozedThreadCount: snoozedThreads.length, }), [ + compact, draggedSettledOrder, routeThreadKey, settledShelfExpanded, @@ -3628,7 +3630,6 @@ export default function Sidebar() { const item = sidebarListItems[args.index]; // Footer rows stay anchored while the main list previews a reorder. if ( - draggingCompactSnoozed || (item?.kind === "thread" ? item.section === "snoozed" : item?.marker === "snoozed-header") @@ -3637,7 +3638,7 @@ export default function Sidebar() { } return sidebarSortingStrategy(args); }, - [draggingCompactSnoozed, sidebarListItems, sidebarSortingStrategy], + [sidebarListItems, sidebarSortingStrategy], ); // Hidden and filtered threads keep their keys. Reserve those slots without // including the rows in the visible drop order or writing to them. @@ -4542,7 +4543,7 @@ export default function Sidebar() { <> 0 ? (
          , ); @@ -4989,7 +4995,7 @@ export default function Sidebar() { key="pinned-divider" marker="pinned-divider" label="Active" - visible={from !== null} + visible={showDragLabels} isDropTarget={dragTargetSection === "active"} />, ); @@ -5075,24 +5081,18 @@ export default function Sidebar() { compact && snoozedFooter ? createPortal(snoozedItems, snoozedFooter, "snoozed-footer") : null, - compact + compactSnoozedDragThread ? createPortal( - {compactSnoozedDragThread ? ( -
            - {renderThreadRowInner( - compactSnoozedDragThread, - "snoozed", - { - isDragging: true, - listeners: undefined, - setNodeRef: () => {}, - transform: null, - transition: undefined, - }, - )} -
          - ) : null} +
            + {renderThreadRowInner(compactSnoozedDragThread, "snoozed", { + isDragging: true, + listeners: undefined, + setNodeRef: () => {}, + transform: null, + transition: undefined, + })} +
          , document.body, "compact-snoozed-drag", From 3daa20bf5825d09946b20ea385de45f156802a82 Mon Sep 17 00:00:00 2001 From: maria-rcks <254055478+maria-rcks@users.noreply.github.com> Date: Sun, 13 Sep 2026 07:55:36 +0000 Subject: [PATCH 10/11] style(web): format compact sidebar sorting --- apps/web/src/components/Sidebar.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 0cafc3f3585b..1b850f1875ba 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -3630,9 +3630,7 @@ export default function Sidebar() { const item = sidebarListItems[args.index]; // Footer rows stay anchored while the main list previews a reorder. if ( - (item?.kind === "thread" - ? item.section === "snoozed" - : item?.marker === "snoozed-header") + item?.kind === "thread" ? item.section === "snoozed" : item?.marker === "snoozed-header" ) { return null; } From 938e1d8dcfe84b16ff7d485f915722cafb12f166 Mon Sep 17 00:00:00 2001 From: maria-rcks <254055478+maria-rcks@users.noreply.github.com> Date: Sun, 13 Sep 2026 15:46:36 +0000 Subject: [PATCH 11/11] fix(web): keep compact sidebar toolbar icons visible --- .../sidebar/SidebarThreadHeader.tsx | 99 +++++++------------ 1 file changed, 38 insertions(+), 61 deletions(-) diff --git a/apps/web/src/components/sidebar/SidebarThreadHeader.tsx b/apps/web/src/components/sidebar/SidebarThreadHeader.tsx index 51c7af57f3eb..d0718d9856f8 100644 --- a/apps/web/src/components/sidebar/SidebarThreadHeader.tsx +++ b/apps/web/src/components/sidebar/SidebarThreadHeader.tsx @@ -10,21 +10,19 @@ * of the sidebar's scope logic. `searchFieldRef` lands on the search field so * the picker's popup can anchor to that width rather than to its 28px trigger. */ -import { EllipsisIcon, FolderPlusIcon, SearchIcon, SquarePenIcon, XIcon } from "lucide-react"; +import { FolderPlusIcon, SearchIcon, SquarePenIcon, XIcon } from "lucide-react"; import { type ComponentProps, type KeyboardEvent as ReactKeyboardEvent, type MouseEvent as ReactMouseEvent, type ReactNode, type RefObject, - useState, } from "react"; import { cn } from "~/lib/utils"; import { useCompactSidebarEnabled } from "../../hooks/useSettings"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; -import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; import { SidebarMenuButton, useSidebar } from "../ui/sidebar"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; @@ -75,7 +73,6 @@ export function SidebarThreadHeader({ const compactEnabled = useCompactSidebarEnabled(); const { state, isMobile, setOpen } = useSidebar(); const compact = compactEnabled && state === "collapsed" && !isMobile; - const [actionsOpen, setActionsOpen] = useState(false); const resultsVisible = isSearching && searchResultCount > 0; // Results shrink as the query narrows, so the active index can outrun the // list; pointing aria-activedescendant at a removed option strands the @@ -85,13 +82,12 @@ export function SidebarThreadHeader({ ? `New thread (${newThreadShortcutLabel})` : "New thread"; - const actions = ( -
          + return ( +
          {compact ? ( { - setActionsOpen(false); setOpen(true); requestAnimationFrame(() => searchInputRef.current?.focus()); }} @@ -99,48 +95,6 @@ export function SidebarThreadHeader({ ) : null} - {hasProjects ? ( - <> - {projectScope} - { - setActionsOpen(false); - onNewProject(); - }} - > - - - - ) : null} - - {newThreadLabel} - - New thread in current project: Shift+click - {newThreadInProjectShortcutLabel ? ` (${newThreadInProjectShortcutLabel})` : ""} - - - ) : ( - newThreadLabel - ) - } - disabled={newThreadDisabled} - onClick={(event) => { - setActionsOpen(false); - onNewThread(event); - }} - > - - -
          - ); - - return ( -
          {/* Segmented well: the icons read as one control instead of three loose buttons competing with the search field beside them. */} - {compact ? ( - - }> - - - - {actions} - - - ) : ( - actions - )} +
          + {hasProjects ? ( + <> + {projectScope} + + + + + ) : null} + + {newThreadLabel} + + New thread in current project: Shift+click + {newThreadInProjectShortcutLabel ? ` (${newThreadInProjectShortcutLabel})` : ""} + + + ) : ( + newThreadLabel + ) + } + disabled={newThreadDisabled} + onClick={onNewThread} + > + + +
          ); }