authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-14 01:21:16-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-14 01:21:16-03:00
logd81278aa6b272bb3d3314266c6d17f22504d19d6
tree8778a96861d26deb211be26f8b2b3d8e568652f5
parent1bbca0e78202c8ece73351fccd29f3c99070b82e
signature Signed by PGP key B5690EEEBB952194

revert(web): remove the compact sidebar (#11685)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

22 files changed, 208 insertions(+), 1036 deletions(-)

apps/desktop/src/settings/DesktopClientSettings.test.ts-1
......@@ -57,7 +57,6 @@ const clientSettings: ClientSettings = {
5757 proactivePanelsEnabled: true,
5858 showSkillsInSlashMenu: false,
5959 providerModelPreferences: {},
60 sidebarCompactThreadRows: false,
6160 sidebarProjectGroupingMode: "repository_path",
6261 sidebarProjectGroupingOverrides: {
6362 "environment-1:/tmp/project-a": "separate",
apps/web/src/components/AppSidebarLayout.tsx+2-7
......@@ -14,11 +14,7 @@ import { getLocalStorageItem, removeLocalStorageItem } from "../hooks/useLocalSt
1414import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings";
1515import { cn, isMacPlatform } from "../lib/utils";
1616import { primaryServerKeybindingsAtom } from "../state/server";
17import {
18 useCompactSidebarEnabled,
19 useEnvironmentIdentificationMode,
20 useLegacySidebarEnabled,
21} from "../hooks/useSettings";
17import { useEnvironmentIdentificationMode, useLegacySidebarEnabled } from "../hooks/useSettings";
2218import {
2319 PanelAnimationSuppressionProvider,
2420 usePanelAnimationSettings,
......@@ -148,7 +144,6 @@ function ProjectProjectionRetention() {
148144export function AppSidebarLayout({ children }: { children: ReactNode }) {
149145 const navigate = useNavigate();
150146 const legacySidebarEnabled = useLegacySidebarEnabled();
151 const compactSidebarEnabled = useCompactSidebarEnabled();
152147 const { active: panelAnimationsActive, durationMs: panelAnimationDurationMs } =
153148 usePanelAnimationSettings();
154149 // Settings routes show the settings nav in place of whichever thread
......@@ -234,7 +229,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
234229 <ProjectProjectionRetention />
235230 <Sidebar
236231 side="left"
237 collapsible={compactSidebarEnabled ? "icon" : "offcanvas"}
232 collapsible="offcanvas"
238233 data-app-sidebar=""
239234 className="border-r border-sidebar-border bg-sidebar text-sidebar-foreground"
240235 resizable={{
apps/web/src/components/LegacySidebar.tsx+25-52
......@@ -199,11 +199,7 @@ import { SidebarChromeFooter, SidebarChromeHeader } from "./sidebar/SidebarChrom
199199import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";
200200import { useIsMobile } from "~/hooks/useMediaQuery";
201201import { CommandDialogTrigger } from "./ui/command";
202import {
203 useClientSettings,
204 useCompactSidebarEnabled,
205 useUpdateClientSettings,
206} from "~/hooks/useSettings";
202import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings";
207203import { primaryServerKeybindingsAtom } from "../state/server";
208204import {
209205 derivePhysicalProjectKey,
......@@ -1198,9 +1194,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
11981194 const router = useRouter();
11991195 const queuePendingFileDrop = useSidebarPendingFileDropStore((s) => s.queuePendingFileDrop);
12001196 const clearPendingFileDrop = useSidebarPendingFileDropStore((s) => s.clearPendingFileDrop);
1201 const { isMobile, setOpenMobile, state, setOpen } = useSidebar();
1202 const compactSidebarEnabled = useCompactSidebarEnabled();
1203 const isCompact = compactSidebarEnabled && !isMobile && state === "collapsed";
1197 const { isMobile, setOpenMobile } = useSidebar();
12041198 const markThreadUnread = useUiStateStore((state) => state.markThreadUnread);
12051199 const setProjectExpanded = useUiStateStore((state) => state.setProjectExpanded);
12061200 const toggleThreadSelection = useThreadSelectionStore((state) => state.toggleThread);
......@@ -1451,13 +1445,10 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
14511445 if (useThreadSelectionStore.getState().hasSelection()) {
14521446 clearSelection();
14531447 }
1454 setProjectExpanded(projectPreferenceKeys, isCompact || !projectExpanded);
1455 if (isCompact) setOpen(true);
1448 setProjectExpanded(projectPreferenceKeys, !projectExpanded);
14561449 },
14571450 [
14581451 clearSelection,
1459 isCompact,
1460 setOpen,
14611452 dragInProgressRef,
14621453 projectExpanded,
14631454 projectPreferenceKeys,
......@@ -1474,17 +1465,9 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
14741465 if (dragInProgressRef.current) {
14751466 return;
14761467 }
1477 setProjectExpanded(projectPreferenceKeys, isCompact || !projectExpanded);
1478 if (isCompact) setOpen(true);
1468 setProjectExpanded(projectPreferenceKeys, !projectExpanded);
14791469 },
1480 [
1481 dragInProgressRef,
1482 isCompact,
1483 projectExpanded,
1484 projectPreferenceKeys,
1485 setOpen,
1486 setProjectExpanded,
1487 ],
1470 [dragInProgressRef, projectExpanded, projectPreferenceKeys, setProjectExpanded],
14881471 );
14891472
14901473 const handleProjectButtonPointerDownCapture = useCallback(
......@@ -2376,10 +2359,8 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
23762359 <>
23772360 <div className="group/project-header relative">
23782361 <SidebarMenuButton
2379 tooltip={project.displayName}
2380 aria-label={project.displayName}
23812362 ref={isManualProjectSorting ? dragHandleProps?.setActivatorNodeRef : undefined}
2382 className={`pr-8 group-hover/project-header:bg-sidebar-row-hover group-hover/project-header:text-sidebar-foreground max-sm:pr-14 group-data-[collapsible=icon]:justify-center ${
2363 className={`pr-8 group-hover/project-header:bg-sidebar-row-hover group-hover/project-header:text-sidebar-foreground max-sm:pr-14 ${
23832364 isManualProjectSorting ? "cursor-grab active:cursor-grabbing" : ""
23842365 }`}
23852366 {...(isManualProjectSorting && dragHandleProps ? dragHandleProps.attributes : {})}
......@@ -2389,7 +2370,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
23892370 onKeyDown={handleProjectButtonKeyDown}
23902371 onContextMenu={handleProjectButtonContextMenu}
23912372 >
2392 {isCompact ? null : !projectExpanded && projectStatus ? (
2373 {!projectExpanded && projectStatus ? (
23932374 <Tooltip>
23942375 <TooltipTrigger
23952376 render={
......@@ -2420,7 +2401,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
24202401 <span className="flex shrink-0">
24212402 <ProjectFavicon project={project} />
24222403 </span>
2423 <span className="flex min-w-0 flex-1 items-center gap-2 group-data-[collapsible=icon]:hidden">
2404 <span className="flex min-w-0 flex-1 items-center gap-2">
24242405 <span className="truncate text-sm font-medium text-sidebar-foreground/90">
24252406 {project.displayName}
24262407 </span>
......@@ -2434,7 +2415,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
24342415 {/* Environment badge – visible by default, crossfades with the
24352416 "new thread" button on hover using the same pointer-events +
24362417 opacity pattern as the thread row archive/timestamp swap. */}
2437 {!isCompact && project.environmentPresence === "remote-only" && (
2418 {project.environmentPresence === "remote-only" && (
24382419 <Tooltip>
24392420 <TooltipTrigger
24402421 render={
......@@ -2460,7 +2441,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
24602441 <Tooltip>
24612442 <TooltipTrigger
24622443 render={
2463 <div className="pointer-events-none absolute top-[calc(50%+1px)] right-0.5 -translate-y-1/2 opacity-0 transition-opacity duration-150 max-sm:pointer-events-auto max-sm:opacity-100 group-hover/project-header:pointer-events-auto group-hover/project-header:opacity-100 group-focus-within/project-header:pointer-events-auto group-focus-within/project-header:opacity-100 group-data-[collapsible=icon]:hidden">
2444 <div className="pointer-events-none absolute top-[calc(50%+1px)] right-0.5 -translate-y-1/2 opacity-0 transition-opacity duration-150 max-sm:pointer-events-auto max-sm:opacity-100 group-hover/project-header:pointer-events-auto group-hover/project-header:opacity-100 group-focus-within/project-header:pointer-events-auto group-focus-within/project-header:opacity-100">
24642445 <button
24652446 type="button"
24662447 aria-label={`Create new thread in ${project.displayName}`}
......@@ -2693,7 +2674,7 @@ function LocalSecondaryStatus() {
26932674 }
26942675
26952676 return (
2696 <SidebarGroup className="px-2 pt-2 pb-0 group-data-[collapsible=icon]:hidden">
2677 <SidebarGroup className="px-2 pt-2 pb-0">
26972678 {connecting.length > 0 ? (
26982679 <Alert
26992680 variant="default"
......@@ -2997,16 +2978,14 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
29972978 render={
29982979 <SidebarMenuButton
29992980 className="focus-visible:ring-0"
3000 tooltip="Search"
3001 aria-label="Search"
30022981 data-testid="command-palette-trigger"
30032982 />
30042983 }
30052984 >
30062985 <SearchIcon />
3007 <span className="flex-1 truncate group-data-[collapsible=icon]:hidden">Search</span>
2986 <span className="flex-1 truncate">Search</span>
30082987 {commandPaletteShortcutLabel ? (
3009 <Kbd className="h-4 min-w-0 rounded-sm px-1.5 text-[10px] group-data-[collapsible=icon]:hidden">
2988 <Kbd className="h-4 min-w-0 rounded-sm px-1.5 text-[10px]">
30102989 {commandPaletteShortcutLabel}
30112990 </Kbd>
30122991 ) : null}
......@@ -3017,7 +2996,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
30172996 }
30182997 >
30192998 {showArm64IntelBuildWarning && arm64IntelBuildWarningDescription ? (
3020 <SidebarGroup className="px-2 pt-2 pb-0 group-data-[collapsible=icon]:hidden">
2999 <SidebarGroup className="px-2 pt-2 pb-0">
30213000 <Alert variant="warning" className="rounded-2xl border-warning/40 bg-warning/8">
30223001 <TriangleAlertIcon />
30233002 <AlertTitle>Intel build on Apple Silicon</AlertTitle>
......@@ -3041,21 +3020,17 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
30413020 ) : null}
30423021 <LocalSecondaryStatus />
30433022 <SidebarGroup className="px-2 py-2">
3044 <div className="mb-1 flex items-center justify-between pl-2 pr-1.5 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0">
3045 <span className="text-xs font-medium text-sidebar-muted-foreground/80 group-data-[collapsible=icon]:hidden">
3046 Projects
3047 </span>
3023 <div className="mb-1 flex items-center justify-between pl-2 pr-1.5">
3024 <span className="text-xs font-medium text-sidebar-muted-foreground/80">Projects</span>
30483025 <div className="flex items-center gap-1">
3049 <div className="group-data-[collapsible=icon]:hidden">
3050 <ProjectSortMenu
3051 projectSortOrder={projectSortOrder}
3052 threadSortOrder={threadSortOrder}
3053 threadPreviewCount={threadPreviewCount}
3054 onProjectSortOrderChange={handleProjectSortOrderChange}
3055 onThreadSortOrderChange={handleThreadSortOrderChange}
3056 onThreadPreviewCountChange={handleThreadPreviewCountChange}
3057 />
3058 </div>
3026 <ProjectSortMenu
3027 projectSortOrder={projectSortOrder}
3028 threadSortOrder={threadSortOrder}
3029 threadPreviewCount={threadPreviewCount}
3030 onProjectSortOrderChange={handleProjectSortOrderChange}
3031 onThreadSortOrderChange={handleThreadSortOrderChange}
3032 onThreadPreviewCountChange={handleThreadPreviewCountChange}
3033 />
30593034 <Tooltip>
30603035 <TooltipTrigger
30613036 render={
......@@ -3152,9 +3127,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
31523127 )}
31533128
31543129 {projectsLength === 0 && (
3155 <div className="px-2 pt-4 text-center text-secondary-label text-xs group-data-[collapsible=icon]:hidden">
3156 No projects yet
3157 </div>
3130 <div className="px-2 pt-4 text-center text-secondary-label text-xs">No projects yet</div>
31583131 )}
31593132 </SidebarGroup>
31603133 </SidebarContent>
apps/web/src/components/Sidebar.drag.ts+6-14
......@@ -103,7 +103,6 @@ export function createSidebarSortingStrategy(input: {
103103 snoozedThreadCount?: number;
104104 cardHeight?: number;
105105 slimHeight?: number;
106 compact?: boolean;
107106 /** Space each pinned boundary opens for its label while dragging. The
108107 * markers stay zero height at rest, so nothing is reserved until pickup. */
109108 boundaryLabelHeight?: number;
......@@ -141,14 +140,11 @@ export function createSidebarSortingStrategy(input: {
141140 else slimHeight ??= rects[index]?.height;
142141 if (item.key !== active.key) groups[item.section].push(item);
143142 }
144 // Compact icons use h-7; expanded cards include their vertical padding.
145 const scale = input.compact
146 ? (cardHeight ?? slimHeight ?? 28) / 28
147 : slimHeight !== undefined
148 ? slimHeight / 36
149 : (headerScale ?? (cardHeight ?? 82) / 82);
150 cardHeight ??= (input.compact ? 28 : 82) * scale;
151 slimHeight ??= (input.compact ? 28 : 36) * scale;
143 // Cards are 4.875rem + 0.25rem padding; slim rows/placeholders are h-9.
144 const scale =
145 slimHeight !== undefined ? slimHeight / 36 : (headerScale ?? (cardHeight ?? 82) / 82);
146 cardHeight ??= 82 * scale;
147 slimHeight ??= 36 * scale;
152148 const labelHeight = (input.boundaryLabelHeight ?? 0) * scale;
153149 const group = groups[target.section];
154150 const order =
......@@ -222,11 +218,7 @@ export function createSidebarSortingStrategy(input: {
222218 // Consume the shelf's auto margin as drag labels and resized rows need
223219 // room, keeping the combined shelves at their measured bottom.
224220 let shelfSpace =
225 !input.compact &&
226 shelfRect &&
227 beforeShelf &&
228 lastRect &&
229 shelfRect.top > beforeShelf.bottom + 1
221 shelfRect && beforeShelf && lastRect && shelfRect.top > beforeShelf.bottom + 1
230222 ? Math.max(
231223 0,
232224 lastRect.bottom - rects[0].top - heights.reduce((sum, height) => sum + height + 1, -1),
apps/web/src/components/Sidebar.tsx+139-603
......@@ -1,12 +1,10 @@
11import { useSupportsMultiplePullRequests } from "~/hooks/useSupportsMultiplePullRequests";
2import { useCompactSidebarEnabled } from "../hooks/useSettings";
32import { resolveThreadCurrentPullRequestLink } from "@t3tools/shared/threadPullRequests";
43import { useAtomValue } from "@effect/atom-react";
54import { replaceComposerContextReferences } from "@t3tools/shared/composerContextReferences";
65import * as Schema from "effect/Schema";
76import {
87 DndContext,
9 DragOverlay,
108 useSensor,
119 useSensors,
1210 type DragEndEvent,
......@@ -74,7 +72,6 @@ import {
7472 type MouseEvent as ReactMouseEvent,
7573 type ReactNode,
7674} from "react";
77import { createPortal } from "react-dom";
7875import { useParams, useRouter } from "@tanstack/react-router";
7976
8077import { useRightPanelStore } from "../rightPanelStore";
......@@ -124,7 +121,6 @@ import { startNewThreadFromContext } from "../lib/chatThreadActions";
124121import { useClientSettings } from "../hooks/useSettings";
125122import { useCopyToClipboard } from "../hooks/useCopyToClipboard";
126123import { useLocalStorage } from "../hooks/useLocalStorage";
127import { SidebarCompletedTime } from "./sidebar/SidebarCompletedTime";
128124import { useNowMinute } from "../hooks/useNowMinute";
129125import { useEnvironments, usePrimaryEnvironmentId } from "../state/environments";
130126import {
......@@ -325,7 +321,6 @@ function SidebarThreadTooltip({
325321 branchMismatch,
326322 terminalStatus,
327323 terminalProcessCount,
328 compactStatus,
329324}: {
330325 thread: SidebarThreadSummary;
331326 project: ProjectFaviconProject | null;
......@@ -342,7 +337,6 @@ function SidebarThreadTooltip({
342337 } | null;
343338 terminalStatus: TerminalStatusIndicator | null;
344339 terminalProcessCount: number;
345 compactStatus?: string | undefined;
346340}) {
347341 const driverKind = providerEntry?.driverKind ?? null;
348342 const supportsMultiplePullRequests = useSupportsMultiplePullRequests(thread.environmentId);
......@@ -359,7 +353,6 @@ function SidebarThreadTooltip({
359353 {thread.title}
360354 </div>
361355 <div className="grid gap-1.5 pl-0.5 text-xs text-muted-foreground">
362 {compactStatus ? <div>{compactStatus}</div> : null}
363356 {projectDisplayName ? (
364357 <div className="flex min-w-0 items-center gap-2">
365358 {project ? <ProjectFavicon project={project} className="size-3 shrink-0" /> : null}
......@@ -509,7 +502,7 @@ function SnoozePopoverButton(props: {
509502type SortableThreadRowBag = Pick<
510503 ReturnType<typeof useSortable>,
511504 "listeners" | "setNodeRef" | "transform" | "transition" | "isDragging"
512> & { hidden?: boolean };
505>;
513506
514507function SortableThreadRow(props: {
515508 id: string;
......@@ -590,18 +583,7 @@ function SidebarSectionPlaceholder(props: {
590583 props.isDropTarget && "border-primary/40 bg-primary/5 text-primary",
591584 )}
592585 >
593 <span className="group-data-[collapsible=icon]:sr-only">{props.label}</span>
594 {props.marker === "settled-placeholder" ? (
595 <CheckIcon
596 aria-hidden
597 className="hidden size-3.5 group-data-[collapsible=icon]:block"
598 />
599 ) : (
600 <Undo2Icon
601 aria-hidden
602 className="hidden size-3.5 group-data-[collapsible=icon]:block"
603 />
604 )}
586 {props.label}
605587 </div>
606588 ) : null}
607589 </SortableSidebarMarker>
......@@ -625,30 +607,19 @@ function SidebarDragBoundary(props: {
625607 className="pointer-events-none relative mx-0.5 -mb-px h-0"
626608 >
627609 {props.visible ? (
628 <div className="sidebar-drag-boundary-label absolute inset-x-2 top-1 flex h-4 items-center gap-2 group-data-[collapsible=icon]:inset-x-0 group-data-[collapsible=icon]:justify-center">
610 <div className="sidebar-drag-boundary-label absolute inset-x-2 top-1 flex h-4 items-center gap-2">
629611 <span
630612 className={cn(
631613 "shrink-0 text-xs font-medium",
632614 props.isDropTarget ? "text-primary" : "text-sidebar-foreground/80",
633615 )}
634616 >
635 <span className="group-data-[collapsible=icon]:sr-only">{props.label}</span>
636 {props.marker === "pinned-header" ? (
637 <PinIcon
638 aria-hidden
639 className="hidden size-3.5 group-data-[collapsible=icon]:block"
640 />
641 ) : (
642 <Undo2Icon
643 aria-hidden
644 className="hidden size-3.5 group-data-[collapsible=icon]:block"
645 />
646 )}
617 {props.label}
647618 </span>
648619 <span
649620 aria-hidden
650621 className={cn(
651 "h-px flex-1 group-data-[collapsible=icon]:hidden",
622 "h-px flex-1",
652623 props.isDropTarget ? "bg-primary/50" : "bg-sidebar-foreground/25",
653624 )}
654625 />
......@@ -669,32 +640,20 @@ function SidebarSectionHeader(props: {
669640 isDropTarget?: boolean;
670641 toggle: { expanded: boolean; onToggle: () => void };
671642}) {
672 const compactEnabled = useCompactSidebarEnabled();
673 const { state, isMobile } = useSidebar();
674 const compact = compactEnabled && state === "collapsed" && !isMobile;
675643 const snoozed = props.marker === "snoozed-header";
676644 const className = cn(
677645 "flex h-full w-full items-center gap-2 px-2 text-left text-xs font-medium",
678 compact && "justify-center px-0",
679646 snoozed ? "text-blue-600 dark:text-blue-400" : "text-sidebar-muted-foreground/60",
680647 props.dragging && "text-sidebar-foreground/80",
681648 props.isDropTarget && "text-primary",
682649 );
683650 const content = (
684651 <>
685 <span className={compact ? "sr-only" : "shrink-0"}>{props.label}</span>
686 {compact ? (
687 snoozed ? (
688 <AlarmClockIcon aria-hidden className="size-3.5" />
689 ) : (
690 <CheckIcon aria-hidden className="size-3.5" />
691 )
692 ) : null}
652 <span className="shrink-0">{props.label}</span>
693653 <span
694654 aria-hidden
695655 className={cn(
696656 "h-px min-w-2 flex-1",
697 compact && "hidden",
698657 snoozed ? "bg-blue-500/20 dark:bg-blue-400/15" : "bg-sidebar-border/60",
699658 props.dragging && "bg-sidebar-foreground/25",
700659 props.isDropTarget && "bg-primary/50",
......@@ -704,7 +663,6 @@ function SidebarSectionHeader(props: {
704663 aria-hidden
705664 className={cn(
706665 "size-3 shrink-0 transition-transform",
707 compact && "hidden",
708666 props.toggle.expanded && "rotate-180",
709667 )}
710668 />
......@@ -716,22 +674,15 @@ function SidebarSectionHeader(props: {
716674 data-testid={`sidebar-${props.marker}`}
717675 className={cn("mx-0.5 h-8", props.className)}
718676 >
719 <Tooltip disabled={!compact}>
720 <TooltipTrigger
721 render={
722 <button
723 type="button"
724 onClick={props.toggle.onToggle}
725 aria-expanded={props.toggle.expanded}
726 data-testid={`sidebar-${snoozed ? "snoozed" : "settled"}-shelf-toggle`}
727 className={cn(className, "cursor-pointer")}
728 />
729 }
730 >
731 {content}
732 </TooltipTrigger>
733 <TooltipPopup side="right">{props.label}</TooltipPopup>
734 </Tooltip>
677 <button
678 type="button"
679 onClick={props.toggle.onToggle}
680 aria-expanded={props.toggle.expanded}
681 data-testid={`sidebar-${snoozed ? "snoozed" : "settled"}-shelf-toggle`}
682 className={cn(className, "cursor-pointer")}
683 >
684 {content}
685 </button>
735686 </SortableSidebarMarker>
736687 );
737688}
......@@ -754,9 +705,6 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: {
754705 onDiscard: (draftId: DraftId) => void;
755706}) {
756707 const { composer, draftId, onDiscard, onNavigate, session } = props;
757 const compactEnabled = useCompactSidebarEnabled();
758 const { state, isMobile } = useSidebar();
759 const compact = compactEnabled && state === "collapsed" && !isMobile;
760708 const promptPreview =
761709 replaceComposerContextReferences(composer.prompt, (occurrence) => occurrence.label)
762710 .trim()
......@@ -795,34 +743,6 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: {
795743 },
796744 [draftId, onDiscard],
797745 );
798 if (compact) {
799 return (
800 <li className="list-none">
801 <Tooltip>
802 <TooltipTrigger
803 render={
804 <button
805 type="button"
806 data-testid="sidebar-draft-row"
807 aria-label={`Draft: ${preview}`}
808 onClick={handleActivate}
809 className={cn(
810 "relative flex h-7 w-full cursor-pointer items-center justify-center rounded-md outline-none hover:bg-sidebar-row-hover focus-visible:ring-2 focus-visible:ring-ring",
811 props.isActive ? "bg-sidebar-row-active" : draftSurfaceClassName,
812 )}
813 />
814 }
815 >
816 <SquarePenIcon aria-hidden className={cn(draftPenClassName, "size-4")} />
817 </TooltipTrigger>
818 <TooltipPopup side="right" className="max-w-80 whitespace-normal">
819 <div className="font-medium">{props.projectDisplayName}</div>
820 <div>{preview}</div>
821 </TooltipPopup>
822 </Tooltip>
823 </li>
824 );
825 }
826746 return (
827747 <li className="list-none py-0.5">
828748 <div
......@@ -1032,7 +952,6 @@ const dropVerbBadge: Record<SidebarDropVerb, ReactNode> = {
1032952const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1033953 thread: SidebarThreadSummary;
1034954 variant: "card" | "slim";
1035 compact: boolean;
1036955 // Slim rows are either settled (action: un-settle) or merely quiet
1037956 // (seen Ready threads — action: settle).
1038957 variantAction: "settle" | "unsettle" | "unsnooze";
......@@ -1113,9 +1032,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
11131032 variant,
11141033 variantAction,
11151034 } = props;
1116 const compactEnabled = useCompactSidebarEnabled();
1117 const { state, isMobile } = useSidebar();
1118 const compact = compactEnabled && state === "collapsed" && !isMobile;
11191035 const threadRef = useMemo(
11201036 () => scopeThreadRef(thread.environmentId, thread.id),
11211037 [thread.environmentId, thread.id],
......@@ -1293,16 +1209,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
12931209 branchMismatch={branchMismatch}
12941210 terminalStatus={terminalStatus}
12951211 terminalProcessCount={terminalProcessCount}
1296 compactStatus={
1297 compact || (props.compact && variant === "card")
1298 ? (topStatus?.label ??
1299 (variantAction === "unsnooze"
1300 ? "Snoozed"
1301 : variantAction === "unsettle"
1302 ? "Settled"
1303 : "Ready"))
1304 : undefined
1305 }
13061212 />
13071213 );
13081214
......@@ -1349,7 +1255,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
13491255 [isRenaming, onStartRename, thread.title, threadRef],
13501256 );
13511257 const [isFileDragOver, setIsFileDragOver] = useState(false);
1352 const [tooltipOpen, setTooltipOpen] = useState(false);
13531258 const fileDropHandlers = useMemo(
13541259 () =>
13551260 onFileDropThreads
......@@ -1512,7 +1417,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
15121417 // A zero-height boundary also makes dnd-kit scale the source to
15131418 // zero. Only projected peers use scaleY as a visibility sentinel.
15141419 visibility:
1515 sortable.hidden || (!sortable.isDragging && sortable.transform?.scaleY === 0)
1420 !sortable.isDragging && sortable.transform?.scaleY === 0
15161421 ? ("hidden" as const)
15171422 : undefined,
15181423 },
......@@ -1584,21 +1489,18 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
15841489 useRightPanelStore.getState().open(threadRef, "pull-requests");
15851490 if (!props.isActive) onThreadActivate(threadRef);
15861491 }, [onThreadActivate, props.isActive, threadRef]);
1587 const renderPrBadge = (iconOnly: boolean, variant: "underline" | "badge" = "underline") =>
1492 const prBadge =
15881493 prBadgeShape?.kind === "stack" || pr || currentLinkedPr ? (
15891494 <ThreadPullRequestBadgeControl
1590 variant={variant}
1495 variant="underline"
15911496 badge={prBadgeShape}
15921497 number={pr?.number ?? currentLinkedPr?.number}
15931498 url={pr?.url ?? currentLinkedPr?.url}
15941499 status={prStatus}
1595 iconOnly={iconOnly}
15961500 onOpenStack={handlePrStackClick}
15971501 onOpenPullRequest={handlePrClick}
15981502 />
15991503 ) : null;
1600 const hasPrBadge = prBadgeShape?.kind === "stack" || pr !== null || currentLinkedPr !== null;
1601 const prBadge = renderPrBadge(false);
16021504 const terminalStatusIcon = terminalStatus ? (
16031505 <span
16041506 role="img"
......@@ -1656,102 +1558,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
16561558 )
16571559 ) : null;
16581560
1659 if (compact) {
1660 return (
1661 <li
1662 data-thread-item
1663 {...sortableRootProps}
1664 {...(fileDropHandlers ?? {})}
1665 className={cn("list-none", sortable?.isDragging && "relative z-20")}
1666 >
1667 <Tooltip
1668 open={
1669 sortable?.hidden ? false : sortable?.isDragging ? dragDestination !== null : tooltipOpen
1670 }
1671 onOpenChange={setTooltipOpen}
1672 >
1673 <TooltipTrigger
1674 render={
1675 <div
1676 ref={rowRef}
1677 role="button"
1678 tabIndex={0}
1679 aria-label={`${thread.title}${topStatus ? `, ${topStatus.label}` : ""}`}
1680 aria-current={props.isActive ? "page" : undefined}
1681 aria-busy={isRegeneratingTitle || undefined}
1682 className={cn(
1683 rowSurfaceClassName,
1684 "flex h-7 items-center justify-center focus-visible:ring-2 focus-visible:ring-ring",
1685 )}
1686 onClick={handleClick}
1687 onDoubleClick={handleDoubleClick}
1688 onKeyDown={handleKeyDown}
1689 onContextMenu={handleContextMenu}
1690 />
1691 }
1692 >
1693 <span className="relative inline-flex size-4 shrink-0 items-center justify-center">
1694 {props.project ? (
1695 <ProjectFavicon project={props.project} className="size-4" />
1696 ) : driverKind ? (
1697 <ProviderInstanceIcon
1698 driverKind={driverKind}
1699 displayName={providerEntry?.displayName ?? modelInstanceId}
1700 iconClassName="size-4"
1701 />
1702 ) : (
1703 <SquarePenIcon aria-hidden className="size-4" />
1704 )}
1705 {isRemote ? (
1706 <Tooltip>
1707 <TooltipTrigger
1708 render={
1709 <span
1710 role="img"
1711 aria-label={props.environmentLabel ?? "Remote environment"}
1712 className="absolute -left-1 -bottom-1 inline-flex size-3 items-center justify-center rounded-full bg-sidebar text-sidebar-muted-foreground ring-1 ring-sidebar"
1713 />
1714 }
1715 >
1716 <EnvironmentMachineIcon kind={props.environmentMachine} className="size-2.5" />
1717 </TooltipTrigger>
1718 <TooltipPopup side="right">
1719 {props.environmentLabel ?? "Remote environment"}
1720 </TooltipPopup>
1721 </Tooltip>
1722 ) : null}
1723 {hasPrBadge ? (
1724 <span className="absolute -top-1 -right-1 inline-flex">
1725 {renderPrBadge(true, "badge")}
1726 </span>
1727 ) : null}
1728 </span>
1729 {topStatus ? (
1730 <span
1731 aria-hidden
1732 className={cn(
1733 "absolute right-0.5 bottom-0.5 size-1.5 rounded-full bg-current",
1734 topStatus.className,
1735 )}
1736 />
1737 ) : hasUnsentDraft ? (
1738 <span
1739 aria-hidden
1740 className="absolute right-0.5 bottom-0.5 size-1.5 rounded-full bg-primary"
1741 />
1742 ) : null}
1743 {props.jumpLabel ? <JumpHintBadge label={props.jumpLabel} /> : null}
1744 </TooltipTrigger>
1745 {sortable?.isDragging ? (
1746 <TooltipPopup side="right">{dragDestination}</TooltipPopup>
1747 ) : (
1748 detailsTooltip
1749 )}
1750 </Tooltip>
1751 </li>
1752 );
1753 }
1754
17551561 if (variant === "slim") {
17561562 return (
17571563 <li
......@@ -1764,11 +1570,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
17641570 sortable?.isDragging && "relative z-20",
17651571 )}
17661572 >
1767 <Tooltip
1768 open={!sortable?.isDragging && tooltipOpen}
1769 onOpenChange={setTooltipOpen}
1770 disabled={sortable?.isDragging}
1771 >
1573 <Tooltip disabled={sortable?.isDragging}>
17721574 <TooltipTrigger
17731575 render={
17741576 <div
......@@ -1908,22 +1710,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
19081710 );
19091711 }
19101712
1911 const compactRows = props.compact;
1912 const CompactStatusIcon = topStatus
1913 ? {
1914 working: CircleDashedIcon,
1915 monitoring: EyeIcon,
1916 approval: ShieldQuestionIcon,
1917 input: MessageCircleQuestionIcon,
1918 failed: CircleAlertIcon,
1919 woke: AlarmClockIcon,
1920 done: CircleCheckIcon,
1921 }[topStatus.icon]
1922 : null;
1923 const compactCompletedAt =
1924 compactRows && status === "ready" && !isWokeStatus
1925 ? (thread.latestTurn?.completedAt ?? null)
1926 : null;
19271713 const diff = latestTurnDiff(thread);
19281714
19291715 return (
......@@ -1932,25 +1718,19 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
19321718 {...sortableRootProps}
19331719 {...(fileDropHandlers ?? {})}
19341720 className={cn(
1935 "list-none [content-visibility:auto]",
1936 compactRows
1937 ? "[contain-intrinsic-size:auto_36px]"
1938 : "py-0.5 [contain-intrinsic-size:auto_78px]",
1721 // Matches the h-[4.875rem] content box; the py-0.5 padding is added on top.
1722 "list-none py-0.5 [content-visibility:auto] [contain-intrinsic-size:auto_78px]",
19391723 sortable?.isDragging && "relative z-20",
19401724 )}
19411725 >
1942 <Tooltip
1943 open={!snoozeMenuOpen && !sortable?.isDragging && tooltipOpen}
1944 onOpenChange={setTooltipOpen}
1945 disabled={snoozeMenuOpen || sortable?.isDragging}
1946 >
1726 <Tooltip disabled={snoozeMenuOpen || sortable?.isDragging}>
19471727 <TooltipTrigger
19481728 render={
19491729 <div
19501730 ref={rowRef}
19511731 role="button"
19521732 tabIndex={0}
1953 data-testid={compactRows ? "sidebar-row-compact" : "sidebar-row-card"}
1733 data-testid="sidebar-row-card"
19541734 aria-busy={isRegeneratingTitle || undefined}
19551735 className={rowSurfaceClassName}
19561736 onClick={handleClick}
......@@ -1960,62 +1740,13 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
19601740 />
19611741 }
19621742 >
1963 <div
1964 className={cn(
1965 "relative z-10",
1966 compactRows
1967 ? "flex h-9 items-center px-2.5"
1968 : "h-[4.875rem] px-[var(--sidebar-row-content-inset)] py-[var(--sidebar-content-inset)]",
1969 )}
1970 >
1971 <div className="flex h-5 w-full min-w-0 items-center gap-1.5">
1743 <div className="relative z-10 h-[4.875rem] px-[var(--sidebar-row-content-inset)] py-[var(--sidebar-content-inset)]">
1744 <div className="flex h-5 min-w-0 items-center gap-1.5">
19721745 {draftIndicator}
19731746 {props.project ? (
1974 <span className="relative inline-flex size-4 shrink-0 items-center justify-center">
1975 <ProjectFavicon project={props.project} className="size-4" />
1976 {compactRows && isRemote ? (
1977 <Tooltip>
1978 <TooltipTrigger
1979 render={
1980 <span
1981 role="img"
1982 aria-label={props.environmentLabel ?? "Remote environment"}
1983 className="absolute -right-1 -bottom-1 inline-flex size-3 items-center justify-center rounded-full bg-sidebar text-sidebar-muted-foreground ring-1 ring-sidebar"
1984 />
1985 }
1986 >
1987 <EnvironmentMachineIcon
1988 kind={props.environmentMachine}
1989 className="size-2.5"
1990 />
1991 </TooltipTrigger>
1992 <TooltipPopup side="top">
1993 {props.environmentLabel ?? "Remote environment"}
1994 </TooltipPopup>
1995 </Tooltip>
1996 ) : null}
1997 </span>
1998 ) : compactRows && isRemote ? (
1999 <Tooltip>
2000 <TooltipTrigger
2001 render={
2002 <span
2003 role="img"
2004 aria-label={props.environmentLabel ?? "Remote environment"}
2005 className="inline-flex size-4 shrink-0 items-center justify-center text-sidebar-muted-foreground/70"
2006 />
2007 }
2008 >
2009 <EnvironmentMachineIcon kind={props.environmentMachine} className="size-3.5" />
2010 </TooltipTrigger>
2011 <TooltipPopup side="top">
2012 {props.environmentLabel ?? "Remote environment"}
2013 </TooltipPopup>
2014 </Tooltip>
1747 <ProjectFavicon project={props.project} className="size-4 shrink-0" />
20151748 ) : null}
2016 {compactRows ? (
2017 title
2018 ) : props.projectDisplayName ? (
1749 {props.projectDisplayName ? (
20191750 <span
20201751 className={cn(
20211752 "min-w-0 flex-1 truncate text-secondary-label text-xs",
......@@ -2028,38 +1759,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
20281759 <span className="flex-1" />
20291760 )}
20301761 {pinIndicator}
2031 {compactRows ? (
2032 <>
2033 {terminalStatusIcon}
2034 {topStatus && CompactStatusIcon ? (
2035 isWokeStatus ? (
2036 <button
2037 type="button"
2038 aria-label="Dismiss Woke notification"
2039 onClick={handleAcknowledgeWokeClick}
2040 className={cn(
2041 "shrink-0 rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring",
2042 topStatus.className,
2043 )}
2044 >
2045 <CompactStatusIcon aria-hidden className="size-3.5" />
2046 <span role="status" className="sr-only">
2047 {topStatus.label}
2048 </span>
2049 </button>
2050 ) : (
2051 <span
2052 role="status"
2053 className={cn("inline-flex shrink-0", topStatus.className)}
2054 >
2055 <CompactStatusIcon aria-hidden className="size-3.5" />
2056 <span className="sr-only">{topStatus.label}</span>
2057 </span>
2058 )
2059 ) : null}
2060 {renderPrBadge(true)}
2061 </>
2062 ) : null}
20631762 {/* The visible state owns this slot's width: status at rest,
20641763 actions on hover/keyboard focus or while the popover is open. Keeping
20651764 the hidden state out of flow lets the project label reclaim
......@@ -2067,35 +1766,20 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
20671766 {sortable?.isDragging ? (
20681767 dragDestination
20691768 ) : (
2070 <span
2071 className={cn(
2072 "group/sidebar-status-slot relative ml-auto flex h-5 shrink-0 items-stretch justify-end text-xs",
2073 !compactRows && "min-w-8",
2074 )}
2075 >
1769 <span className="group/sidebar-status-slot relative ml-auto flex h-5 min-w-8 shrink-0 items-stretch justify-end text-xs">
20761770 {/* Read-only status labels yield to the hover actions. Woke is
20771771 itself an action, so it stays pointer-enabled and visible
20781772 while the other controls appear beside it. */}
20791773 <span
20801774 className={cn(
2081 isWokeStatus && !compactRows
1775 isWokeStatus
20821776 ? "pointer-events-auto"
20831777 : "pointer-events-none group-has-[:focus-visible]/sidebar-status-slot:absolute group-has-[:focus-visible]/sidebar-status-slot:right-0 group-has-[:focus-visible]/sidebar-status-slot:opacity-0 group-hover/sidebar-row:absolute group-hover/sidebar-row:right-0 group-hover/sidebar-row:opacity-0",
20841778 "flex items-center self-center justify-self-end tabular-nums text-secondary-label transition-opacity",
20851779 snoozeMenuOpen && "pointer-events-none absolute right-0 opacity-0",
20861780 )}
20871781 >
2088 {compactRows ? (
2089 status === "working" ? (
2090 <span className={topStatus?.className}>
2091 <WorkingDuration startedAt={resolveWorkingStartedAt(thread)} />
2092 </span>
2093 ) : compactCompletedAt ? (
2094 <SidebarCompletedTime completedAt={compactCompletedAt} />
2095 ) : (
2096 threadTimeLabel(thread)
2097 )
2098 ) : topStatus ? (
1782 {topStatus ? (
20991783 isWokeStatus ? (
21001784 <Tooltip>
21011785 <TooltipTrigger
......@@ -2201,7 +1885,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
22011885 }
22021886 >
22031887 <CheckIcon className="size-3.5" />
2204 {compactRows ? null : "Settle"}
1888 Settle
22051889 </TooltipTrigger>
22061890 <TooltipPopup>Settle thread</TooltipPopup>
22071891 </Tooltip>
......@@ -2211,70 +1895,68 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
22111895 </span>
22121896 )}
22131897 </div>
2214 {isRegeneratingTitle ? (
2215 <span role="status" className="sr-only">
2216 Regenerating title
2217 </span>
2218 ) : null}
2219 {compactRows ? null : (
2220 <>
2221 <div className="mt-1 flex min-w-0">{title}</div>
2222 <div className="mt-0.5 flex min-w-0 items-center gap-1.5 text-secondary-label text-xs">
2223 {/* Always the branch. The plan step used to take this slot while
1898 <div className="mt-1 flex min-w-0">
1899 {title}
1900 {isRegeneratingTitle ? (
1901 <span role="status" className="sr-only">
1902 Regenerating title
1903 </span>
1904 ) : null}
1905 </div>
1906 <div className="mt-0.5 flex min-w-0 items-center gap-1.5 text-secondary-label text-xs">
1907 {/* Always the branch. The plan step used to take this slot while
22241908 working, but it truncated to a half-sentence and dropped the
22251909 branch, so the row lost its most stable identifier. */}
2226 {thread.branch ? (
2227 <>
2228 <ThreadWorktreeIndicator thread={thread} />
2229 <span className="min-w-0 flex-1 truncate whitespace-nowrap text-muted-foreground/40">
2230 {thread.branch}
2231 </span>
2232 </>
2233 ) : (
2234 <span className="flex-1" />
2235 )}
2236 {terminalStatusIcon}
2237 {prBadge}
2238 {diff ? (
2239 <span className="shrink-0 font-mono">
2240 <span className="text-diff-addition-foreground">+{diff.insertions}</span>{" "}
2241 <span className="text-diff-deletion-foreground">−{diff.deletions}</span>
2242 </span>
2243 ) : null}
2244 <span
2245 aria-hidden
2246 className="pointer-events-none ml-auto inline-flex shrink-0 items-center gap-1"
2247 >
2248 {isRemote ? (
2249 <span className="inline-flex shrink-0 items-center text-sidebar-muted-foreground/70">
2250 <EnvironmentMachineIcon
2251 aria-hidden
2252 kind={props.environmentMachine}
2253 className="size-3.5"
2254 />
2255 </span>
2256 ) : null}
2257 {driverKind ? (
2258 <span className="inline-flex shrink-0 items-center">
2259 <ProviderInstanceIcon
2260 driverKind={driverKind}
2261 displayName={
2262 providerEntry?.displayName ??
2263 thread.session?.providerName ??
2264 modelInstanceId
2265 }
2266 accentColor={providerEntry?.accentColor}
2267 showBadge={showInstanceBadge}
2268 // Glyph dims, badge stays saturated; offset matches the composer trigger.
2269 iconClassName="size-3.5 opacity-60"
2270 badgeClassName="right-[-0.1875rem] bottom-[-0.1875rem] h-3 min-w-3 px-0.5 text-[7px]"
2271 />
2272 </span>
2273 ) : null}
1910 {thread.branch ? (
1911 <>
1912 <ThreadWorktreeIndicator thread={thread} />
1913 <span className="min-w-0 flex-1 truncate whitespace-nowrap text-muted-foreground/40">
1914 {thread.branch}
22741915 </span>
2275 </div>
2276 </>
2277 )}
1916 </>
1917 ) : (
1918 <span className="flex-1" />
1919 )}
1920 {terminalStatusIcon}
1921 {prBadge}
1922 {diff ? (
1923 <span className="shrink-0 font-mono">
1924 <span className="text-diff-addition-foreground">+{diff.insertions}</span>{" "}
1925 <span className="text-diff-deletion-foreground">−{diff.deletions}</span>
1926 </span>
1927 ) : null}
1928 <span
1929 aria-hidden
1930 className="pointer-events-none ml-auto inline-flex shrink-0 items-center gap-1"
1931 >
1932 {isRemote ? (
1933 <span className="inline-flex shrink-0 items-center text-sidebar-muted-foreground/70">
1934 <EnvironmentMachineIcon
1935 aria-hidden
1936 kind={props.environmentMachine}
1937 className="size-3.5"
1938 />
1939 </span>
1940 ) : null}
1941 {driverKind ? (
1942 <span className="inline-flex shrink-0 items-center">
1943 <ProviderInstanceIcon
1944 driverKind={driverKind}
1945 displayName={
1946 providerEntry?.displayName ??
1947 thread.session?.providerName ??
1948 modelInstanceId
1949 }
1950 accentColor={providerEntry?.accentColor}
1951 showBadge={showInstanceBadge}
1952 // Glyph dims, badge stays saturated; offset matches the composer trigger.
1953 iconClassName="size-3.5 opacity-60"
1954 badgeClassName="right-[-0.1875rem] bottom-[-0.1875rem] h-3 min-w-3 px-0.5 text-[7px]"
1955 />
1956 </span>
1957 ) : null}
1958 </span>
1959 </div>
22781960 </div>
22791961 {props.jumpLabel ? <JumpHintBadge label={props.jumpLabel} /> : null}
22801962 </TooltipTrigger>
......@@ -2308,9 +1990,6 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {
23081990 onFileDropThreads: (threadRef: ScopedThreadRef, files: File[]) => void;
23091991}) {
23101992 const { thread } = props;
2311 const compactEnabled = useCompactSidebarEnabled();
2312 const { state, isMobile } = useSidebar();
2313 const compact = compactEnabled && state === "collapsed" && !isMobile;
23141993 const threadRef = useMemo(
23151994 () => scopeThreadRef(thread.environmentId, thread.id),
23161995 [thread.environmentId, thread.id],
......@@ -2396,7 +2075,6 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {
23962075 onClick={props.onSelect}
23972076 className={cn(
23982077 "flex h-9 w-full cursor-pointer items-center gap-2.5 rounded-md px-2.5 text-left text-sm outline-none",
2399 compact && "justify-center px-0",
24002078 props.isHighlighted || props.isRouteActive
24012079 ? "bg-sidebar-row-active text-sidebar-foreground"
24022080 : "text-sidebar-muted-foreground/75 hover:bg-sidebar-row-hover hover:text-sidebar-foreground",
......@@ -2408,15 +2086,9 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {
24082086 >
24092087 {props.project ? (
24102088 <ProjectFavicon project={props.project} className="size-4 shrink-0" />
2411 ) : compact ? (
2412 <SquarePenIcon aria-hidden className="size-4" />
24132089 ) : null}
2414 <span className={compact ? "sr-only" : "min-w-0 flex-1 truncate"}>{thread.title}</span>
2415 <span
2416 className={
2417 compact ? "sr-only" : "shrink-0 text-xs text-muted-foreground/55 tabular-nums"
2418 }
2419 >
2090 <span className="min-w-0 flex-1 truncate">{thread.title}</span>
2091 <span className="shrink-0 text-xs text-muted-foreground/55 tabular-nums">
24202092 {threadTimeLabel(thread)}
24212093 </span>
24222094 </TooltipTrigger>
......@@ -2444,12 +2116,8 @@ export default function Sidebar() {
24442116 const projectOrder = useUiStateStore((store) => store.projectOrder);
24452117 const threads = useThreadShells();
24462118 const router = useRouter();
2447 const { isMobile, setOpenMobile, setOpen, state: sidebarState } = useSidebar();
2448 const compactEnabled = useCompactSidebarEnabled();
2449 const compact = compactEnabled && sidebarState === "collapsed" && !isMobile;
2450 const [snoozedFooter, setSnoozedFooter] = useState<HTMLUListElement | null>(null);
2119 const { isMobile, setOpenMobile } = useSidebar();
24512120 const keybindings = useAtomValue(primaryServerKeybindingsAtom);
2452 const compactThreadRows = useClientSettings((s) => s.sidebarCompactThreadRows);
24532121 const confirmThreadDelete = useClientSettings((s) => s.confirmThreadDelete);
24542122 const confirmThreadArchive = useClientSettings((s) => s.confirmThreadArchive);
24552123 const sidebarProjectSortOrder = useClientSettings((s) => s.sidebarProjectSortOrder);
......@@ -3020,7 +2688,6 @@ export default function Sidebar() {
30202688 [setSettledShelfExpanded],
30212689 );
30222690 const renderedSettledThreads = useMemo(() => {
3023 if (compact) return EMPTY_THREADS;
30242691 if (settledShelfExpanded) return visibleSettledThreads;
30252692 if (routeThreadKey === null) return EMPTY_THREADS;
30262693 const routeThread = visibleSettledThreads.find(
......@@ -3028,7 +2695,7 @@ export default function Sidebar() {
30282695 scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)) === routeThreadKey,
30292696 );
30302697 return routeThread === undefined ? EMPTY_THREADS : [routeThread];
3031 }, [compact, routeThreadKey, settledShelfExpanded, visibleSettledThreads]);
2698 }, [routeThreadKey, settledShelfExpanded, visibleSettledThreads]);
30322699
30332700 // The snoozed shelf is collapsed by default: out of the way, never gone.
30342701 // Collapsed threads don't render (and so don't participate in jump
......@@ -3256,14 +2923,10 @@ export default function Sidebar() {
32562923
32572924 const [renamingThreadKey, setRenamingThreadKey] = useState<string | null>(null);
32582925 const [renamingTitle, setRenamingTitle] = useState("");
3259 const startThreadRename = useCallback(
3260 (threadRef: ScopedThreadRef, title: string) => {
3261 if (compact) setOpen(true);
3262 setRenamingThreadKey(scopedThreadKey(threadRef));
3263 setRenamingTitle(title);
3264 },
3265 [compact, setOpen],
3266 );
2926 const startThreadRename = useCallback((threadRef: ScopedThreadRef, title: string) => {
2927 setRenamingThreadKey(scopedThreadKey(threadRef));
2928 setRenamingTitle(title);
2929 }, []);
32672930 const cancelThreadRename = useCallback(() => setRenamingThreadKey(null), []);
32682931 const commitThreadRename = useCallback(
32692932 (threadRef: ScopedThreadRef, title: string, originalTitle: string) => {
......@@ -3425,18 +3088,8 @@ export default function Sidebar() {
34253088 const threadListRef = useRef<HTMLUListElement | null>(null);
34263089 const dragLabelOffsetRef = useRef(0);
34273090 const restrictBelowPins = useCallback<Modifier>(
3428 (args) =>
3429 restrictBelowSidebarLabel(
3430 {
3431 ...args,
3432 // The fixed snoozed shelf shares the main list's drag boundary.
3433 containerNodeRect: compact
3434 ? (threadListRef.current?.getBoundingClientRect() ?? args.containerNodeRect)
3435 : args.containerNodeRect,
3436 },
3437 dragLabelOffsetRef.current,
3438 ),
3439 [compact],
3091 (args) => restrictBelowSidebarLabel(args, dragLabelOffsetRef.current),
3092 [],
34403093 );
34413094 const listMotionRef = useRef<ReturnType<typeof createSidebarListMotion> | null>(null);
34423095 const attachListMotionRef = useCallback((node: HTMLUListElement | null) => {
......@@ -3658,7 +3311,7 @@ export default function Sidebar() {
36583311 pinnedThreads.length +
36593312 activeThreads.length +
36603313 snoozedThreads.length +
3661 (compact ? 0 : settledThreads.length) ===
3314 settledThreads.length ===
36623315 0
36633316 ) {
36643317 return [];
......@@ -3674,15 +3327,13 @@ export default function Sidebar() {
36743327 items.push({ kind: "marker", marker: "snoozed-header" });
36753328 items.push(...rowsOf(visibleSnoozedThreads, "snoozed"));
36763329 }
3677 if (!compact) {
3678 items.push({ kind: "marker", marker: "settled-header" });
3679 items.push({ kind: "marker", marker: "settled-placeholder" });
3680 items.push(...rowsOf(renderedSettledThreads, "settled"));
3681 }
3330 items.push({ kind: "marker", marker: "settled-header" });
3331 const settledRows = rowsOf(renderedSettledThreads, "settled");
3332 items.push({ kind: "marker", marker: "settled-placeholder" });
3333 items.push(...settledRows);
36823334 return items;
36833335 }, [
36843336 activeThreads,
3685 compact,
36863337 pinnedThreads,
36873338 renderedSettledThreads,
36883339 settledThreads.length,
......@@ -3752,7 +3403,6 @@ export default function Sidebar() {
37523403 () =>
37533404 createSidebarSortingStrategy({
37543405 items: sidebarListItems,
3755 compact,
37563406 boundaryLabelHeight: SIDEBAR_DRAG_LABEL_HEIGHT,
37573407 settledOrder: draggedSettledOrder,
37583408 settledExpanded: settledShelfExpanded,
......@@ -3761,7 +3411,6 @@ export default function Sidebar() {
37613411 snoozedThreadCount: snoozedThreads.length,
37623412 }),
37633413 [
3764 compact,
37653414 draggedSettledOrder,
37663415 routeThreadKey,
37673416 settledShelfExpanded,
......@@ -3770,22 +3419,6 @@ export default function Sidebar() {
37703419 snoozedThreads.length,
37713420 ],
37723421 );
3773 const draggingCompactSnoozed = compact && dragState?.activeSection === "snoozed";
3774 const compactSnoozedDragThread =
3775 draggingCompactSnoozed && dragState ? threadByKey.get(dragState.activeKey) : undefined;
3776 const compactSidebarSortingStrategy = useCallback<typeof sidebarSortingStrategy>(
3777 (args) => {
3778 const item = sidebarListItems[args.index];
3779 // Footer rows stay anchored while the main list previews a reorder.
3780 if (
3781 item?.kind === "thread" ? item.section === "snoozed" : item?.marker === "snoozed-header"
3782 ) {
3783 return null;
3784 }
3785 return sidebarSortingStrategy(args);
3786 },
3787 [sidebarListItems, sidebarSortingStrategy],
3788 );
37893422 // Hidden and filtered threads keep their keys. Reserve those slots without
37903423 // including the rows in the visible drop order or writing to them.
37913424 const { pinnedKeysById, activeKeysById } = useMemo(
......@@ -4689,19 +4322,7 @@ export default function Sidebar() {
46894322 <>
46904323 <SidebarChromeHeader isElectron={isElectron} />
46914324 <SidebarContent
4692 className={cn(
4693 "gap-0",
4694 compact ? "group-data-[collapsible=icon]:overflow-visible" : "min-h-full",
4695 )}
4696 fixedFooter={
4697 compact && !isSearchingThreads && snoozedThreads.length > 0 ? (
4698 <ul
4699 ref={setSnoozedFooter}
4700 aria-label="Snoozed threads"
4701 className="relative flex max-h-[min(30vh,16rem)] flex-col gap-px overflow-y-auto px-[var(--sidebar-content-inset)] pb-1"
4702 />
4703 ) : null
4704 }
4325 className="gap-0 min-h-full"
47054326 fixedHeader={
47064327 // Lifted above the stage backdrop, whose fade bleeds below the
47074328 // header and would otherwise paint across the search row's outline.
......@@ -4761,12 +4382,8 @@ export default function Sidebar() {
47614382 // popup opens under the field, is at least as wide as it,
47624383 // and grows to fit project names up to a cap, past which
47634384 // the rows truncate.
4764 anchor={compact ? undefined : headerSearchRef}
4765 side={compact ? "right" : "bottom"}
4766 className={cn(
4767 "max-w-[min(18rem,var(--available-width))] overflow-hidden",
4768 compact && "min-w-56",
4769 )}
4385 anchor={headerSearchRef}
4386 className="max-w-[min(18rem,var(--available-width))] overflow-hidden"
47704387 >
47714388 <ComboboxSearchInput
47724389 aria-label="Search projects"
......@@ -4868,12 +4485,7 @@ export default function Sidebar() {
48684485 </SidebarGroup>
48694486 }
48704487 >
4871 <SidebarGroup
4872 className={cn(
4873 "ps-[calc(var(--sidebar-content-inset)+1px)] pe-[var(--sidebar-content-inset)] pb-1 pt-0",
4874 !compact && "flex-1",
4875 )}
4876 >
4488 <SidebarGroup className="ps-[calc(var(--sidebar-content-inset)+1px)] pe-[var(--sidebar-content-inset)] pb-1 pt-0 flex-1">
48774489 {isSearchingThreads ? (
48784490 threadSearchResults.length > 0 ? (
48794491 <TooltipProvider
......@@ -4926,10 +4538,7 @@ export default function Sidebar() {
49264538 ) : (
49274539 <p
49284540 role="status"
4929 className={cn(
4930 "px-2 py-6 text-center text-xs text-sidebar-muted-foreground",
4931 compact && "sr-only",
4932 )}
4541 className="px-2 py-6 text-center text-xs text-sidebar-muted-foreground"
49334542 >
49344543 No threads found
49354544 </p>
......@@ -4948,23 +4557,20 @@ export default function Sidebar() {
49484557 modifiers={[
49494558 restrictToVerticalAxis,
49504559 restrictBelowPins,
4951 ...(compact ? [] : [restrictToFirstScrollableAncestor]),
4560 restrictToFirstScrollableAncestor,
49524561 ]}
49534562 onDragStart={handleThreadDragStart}
49544563 onDragOver={handleThreadDragOver}
49554564 onDragEnd={handleThreadDragEnd}
49564565 >
49574566 <SidebarDragLifecycle onUnmount={cancelThreadDrag} />
4958 <SortableContext
4959 items={sortableIds}
4960 strategy={compact ? compactSidebarSortingStrategy : sidebarSortingStrategy}
4961 >
4567 <SortableContext items={sortableIds} strategy={sidebarSortingStrategy}>
49624568 <ul
49634569 ref={attachListMotionRef}
49644570 role="list"
49654571 className={cn(
49664572 "relative flex flex-col gap-px",
4967 !compact && sidebarListItems.length > 0 && "flex-1",
4573 sidebarListItems.length > 0 && "flex-1",
49684574 )}
49694575 >
49704576 {(() => {
......@@ -4976,9 +4582,10 @@ export default function Sidebar() {
49764582 const threadKey = scopedThreadKey(
49774583 scopeThreadRef(thread.environmentId, thread.id),
49784584 );
4979 // Settled and snoozed always use slim rows. Active and
4980 // pinned threads use cards unless the user has explicitly
4981 // enabled the compact thread-list preference.
4585 // Settled and snoozed are the ONLY things that collapse a
4586 // row: every other thread is a full card. Density comes
4587 // from users (or the auto rules) actually parking work,
4588 // not from the sidebar second-guessing what still matters.
49824589 const isCard = section === "active" || section === "pinned";
49834590 const rowVariant = isCard ? "card" : "slim";
49844591 return (
......@@ -4988,7 +4595,6 @@ export default function Sidebar() {
49884595 key={`${threadKey}:${rowVariant}`}
49894596 thread={thread}
49904597 variant={rowVariant}
4991 compact={compactThreadRows}
49924598 // Snoozed rows wake, settled rows un-settle, and cards settle.
49934599 variantAction={
49944600 section === "snoozed"
......@@ -5091,25 +4697,11 @@ export default function Sidebar() {
50914697 !draggableThreadKeys.has(threadKey) || optimisticDrop !== null
50924698 }
50934699 >
5094 {(bag) =>
5095 renderThreadRowInner(
5096 thread,
5097 section,
5098 draggingCompactSnoozed && bag.isDragging
5099 ? { ...bag, hidden: true }
5100 : bag,
5101 )
5102 }
4700 {(bag) => renderThreadRowInner(thread, section, bag)}
51034701 </SortableThreadRow>
51044702 );
51054703 };
51064704 const from = dragState?.activeSection ?? null;
5107 const showDragLabels =
5108 from !== null &&
5109 (!compact ||
5110 dragTargetSection === "active" ||
5111 dragTargetSection === "pinned");
5112 const snoozedItems: ReactNode[] = [];
51134705 const items: ReactNode[] = [
51144706 <SidebarDraftBlock
51154707 key="draft-sessions"
......@@ -5121,17 +4713,8 @@ export default function Sidebar() {
51214713 />,
51224714 ];
51234715 for (const item of sidebarListItems) {
5124 const destination =
5125 compact &&
5126 (item.kind === "thread"
5127 ? item.section === "snoozed"
5128 : item.marker === "snoozed-header")
5129 ? snoozedItems
5130 : items;
51314716 if (item.kind === "thread") {
5132 destination.push(
5133 renderThreadRow(threadByKey.get(item.key)!, item.section),
5134 );
4717 items.push(renderThreadRow(threadByKey.get(item.key)!, item.section));
51354718 continue;
51364719 }
51374720 switch (item.marker) {
......@@ -5141,7 +4724,7 @@ export default function Sidebar() {
51414724 key="pinned-header"
51424725 marker="pinned-header"
51434726 label="Pinned"
5144 visible={showDragLabels}
4727 visible={from !== null}
51454728 isDropTarget={dragTargetSection === "pinned"}
51464729 />,
51474730 );
......@@ -5152,7 +4735,7 @@ export default function Sidebar() {
51524735 key="pinned-divider"
51534736 marker="pinned-divider"
51544737 label="Active"
5155 visible={showDragLabels}
4738 visible={from !== null}
51564739 isDropTarget={dragTargetSection === "active"}
51574740 />,
51584741 );
......@@ -5176,11 +4759,11 @@ export default function Sidebar() {
51764759 );
51774760 break;
51784761 case "snoozed-header":
5179 destination.push(
4762 items.push(
51804763 <SidebarSectionHeader
51814764 key="snoozed-shelf-header"
51824765 marker="snoozed-header"
5183 className={cn(!compact && "mt-auto")}
4766 className="mt-auto"
51844767 label={
51854768 snoozedShelfExpanded
51864769 ? "Snoozed"
......@@ -5198,7 +4781,7 @@ export default function Sidebar() {
51984781 <SidebarSectionHeader
51994782 key="settled-shelf-header"
52004783 marker="settled-header"
5201 className={cn(!compact && snoozedThreads.length === 0 && "mt-auto")}
4784 className={cn(snoozedThreads.length === 0 && "mt-auto")}
52024785 label={
52034786 settledShelfExpanded
52044787 ? "Settled"
......@@ -5233,56 +4816,18 @@ export default function Sidebar() {
52334816 break;
52344817 }
52354818 }
5236 // Keep the shelf inside this drag context while anchoring
5237 // its DOM outside the main thread scroller.
5238 return [
5239 ...items,
5240 compact && snoozedFooter
5241 ? createPortal(snoozedItems, snoozedFooter, "snoozed-footer")
5242 : null,
5243 compactSnoozedDragThread
5244 ? createPortal(
5245 <DragOverlay dropAnimation={null}>
5246 <ul className="pointer-events-none">
5247 {renderThreadRowInner(compactSnoozedDragThread, "snoozed", {
5248 isDragging: true,
5249 listeners: undefined,
5250 setNodeRef: () => {},
5251 transform: null,
5252 transition: undefined,
5253 })}
5254 </ul>
5255 </DragOverlay>,
5256 document.body,
5257 "compact-snoozed-drag",
5258 )
5259 : null,
5260 ];
4819 return items;
52614820 })()}
5262 {!compact && settledShelfExpanded && hiddenSettledCount > 0 ? (
4821 {settledShelfExpanded && hiddenSettledCount > 0 ? (
52634822 <li className="list-none">
5264 <Tooltip disabled={!compact}>
5265 <TooltipTrigger
5266 render={
5267 <button
5268 type="button"
5269 onClick={showMoreSettled}
5270 className={cn(
5271 "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",
5272 compact && "justify-center px-0",
5273 )}
5274 />
5275 }
5276 >
5277 <PlusIcon aria-hidden className="size-4 shrink-0" />
5278 <span className={compact ? "sr-only" : undefined}>
5279 Show {Math.min(hiddenSettledCount, SETTLED_TAIL_PAGE_COUNT)} more
5280 </span>
5281 </TooltipTrigger>
5282 <TooltipPopup side="right">
5283 Show {Math.min(hiddenSettledCount, SETTLED_TAIL_PAGE_COUNT)} more
5284 </TooltipPopup>
5285 </Tooltip>
4823 <button
4824 type="button"
4825 onClick={showMoreSettled}
4826 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"
4827 >
4828 <PlusIcon aria-hidden className="size-4 shrink-0" />
4829 Show {Math.min(hiddenSettledCount, SETTLED_TAIL_PAGE_COUNT)} more
4830 </button>
52864831 </li>
52874832 ) : null}
52884833 </ul>
......@@ -5297,29 +4842,20 @@ export default function Sidebar() {
52974842 snoozedThreads.length +
52984843 settledThreads.length ===
52994844 0 ? (
5300 <div
5301 className={cn(
5302 "flex flex-col items-center gap-2 px-2 py-6 text-center text-xs text-muted-foreground/60",
5303 compact && "px-0",
5304 )}
5305 >
4845 <div className="flex flex-col items-center gap-2 px-2 py-6 text-center text-xs text-muted-foreground/60">
53064846 {projects.length === 0 ? (
53074847 <>
5308 <span className={compact ? "sr-only" : undefined}>No projects yet</span>
4848 <span>No projects yet</span>
53094849 <button
53104850 type="button"
53114851 onClick={openAddProjectCommandPalette}
5312 aria-label="Add project"
5313 className={cn(
5314 "inline-flex cursor-pointer 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",
5315 compact && "justify-center px-1.5",
5316 )}
4852 className="inline-flex cursor-pointer 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"
53174853 >
53184854 <PlusIcon className="-mx-0.5 size-3" />
5319 <span className={compact ? "sr-only" : undefined}>Add project</span>
4855 Add project
53204856 </button>
53214857 </>
5322 ) : compact ? null : scopedProjectGroup ? (
4858 ) : scopedProjectGroup ? (
53234859 `No threads in ${scopedProjectGroup.displayName} yet`
53244860 ) : (
53254861 "No threads yet"
apps/web/src/components/ThreadStatusIndicators.tsx+4-12
......@@ -144,17 +144,14 @@ export function ThreadPullRequestBadgeControl({
144144 number,
145145 url,
146146 status,
147 iconOnly = false,
148147 onOpenStack,
149148 onOpenPullRequest,
150149}: {
151 variant: "underline" | "ghost" | "badge";
150 variant: "underline" | "ghost";
152151 badge: ThreadPullRequestBadge | null;
153152 number?: number | undefined;
154153 url?: string | undefined;
155154 status: PrStatusIndicator | null;
156 /** Dense rows drop the number/layer count and keep only the state glyph. */
157 iconOnly?: boolean;
158155 onOpenStack: () => void;
159156 onOpenPullRequest: (event: MouseEvent<HTMLAnchorElement>) => void;
160157}) {
......@@ -171,9 +168,7 @@ export function ThreadPullRequestBadgeControl({
171168 const className = cn(
172169 variant === "ghost"
173170 ? buttonVariants({ variant: "ghost", size: "xs" })
174 : variant === "badge"
175 ? "inline-flex size-3 shrink-0 cursor-pointer items-center justify-center rounded-full bg-sidebar ring-1 ring-sidebar outline-none focus-visible:ring-2 focus-visible:ring-ring"
176 : "inline-flex shrink-0 cursor-pointer items-center gap-0.5 whitespace-nowrap border-b border-transparent hover:border-current focus-visible:outline-2 focus-visible:outline-ring",
171 : "inline-flex shrink-0 cursor-pointer items-center gap-0.5 whitespace-nowrap border-b border-transparent hover:border-current focus-visible:outline-2 focus-visible:outline-ring",
177172 "text-xs tabular-nums",
178173 variant === "ghost" &&
179174 "font-normal text-xs! active:scale-100 [--control-icon-color:currentColor]",
......@@ -183,11 +178,8 @@ export function ThreadPullRequestBadgeControl({
183178 );
184179 const content = (
185180 <>
186 <ThreadPullRequestBadgeIcon
187 icon={badge?.kind ?? "pull-request"}
188 className={variant === "badge" ? "size-2.5" : undefined}
189 />
190 {iconOnly ? null : isStack ? badge.layers : linkedCount !== null ? `+${linkedCount}` : number}
181 <ThreadPullRequestBadgeIcon icon={badge?.kind ?? "pull-request"} />
182 {isStack ? badge.layers : linkedCount !== null ? `+${linkedCount}` : number}
191183 </>
192184 );
193185 return (
apps/web/src/components/settings/CompactSidebarPreview.tsx deleted-82
......@@ -1,82 +0,0 @@
1import { useEffect, useRef, useState } from "react";
2
3import { cn } from "~/lib/utils";
4
5export function CompactSidebarPreview({
6 railEnabled,
7 compactRows,
8}: {
9 railEnabled: boolean;
10 compactRows: boolean;
11}) {
12 const [collapsed, setCollapsed] = useState(false);
13 const sidebarRef = useRef<HTMLSpanElement>(null);
14
15 useEffect(() => {
16 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
17 const animation = sidebarRef.current?.animate(
18 [
19 { width: "36px", offset: 0 },
20 { width: railEnabled ? "12px" : "0px", offset: 0.45 },
21 { width: railEnabled ? "12px" : "0px", offset: 0.6 },
22 { width: "36px", offset: 1 },
23 ],
24 { duration: 800, easing: "ease-in-out" },
25 );
26 return () => animation?.cancel();
27 }, [railEnabled, compactRows]);
28
29 return (
30 <button
31 type="button"
32 aria-label="Toggle compact sidebar preview"
33 aria-pressed={collapsed}
34 className="group/compact-preview relative flex h-10 w-full cursor-pointer overflow-hidden rounded-lg border border-border bg-background p-1 shadow-xs/5 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background"
35 onClick={() => {
36 sidebarRef.current?.getAnimations().forEach((animation) => animation.cancel());
37 setCollapsed((value) => !value);
38 }}
39 >
40 <span
41 ref={sidebarRef}
42 aria-hidden
43 className={cn(
44 "flex h-full shrink-0 flex-col justify-center gap-1 overflow-hidden rounded-md bg-sidebar transition-[width] duration-250 ease-out motion-reduce:transition-none",
45 collapsed ? (railEnabled ? "w-3" : "w-0") : "w-9",
46 )}
47 >
48 {(compactRows ? [0, 1, 2, 3] : [0, 1, 2]).map((row) => (
49 <span key={row} className="flex shrink-0 items-start gap-1 px-1">
50 <span className="size-1 shrink-0 rounded-[1px] bg-muted-foreground/50" />
51 <span
52 className={cn(
53 "flex shrink-0 flex-col gap-0.5 transition-opacity duration-250 ease-out motion-reduce:transition-none",
54 collapsed && "opacity-0",
55 )}
56 >
57 <span className="h-0.5 w-4 rounded-full bg-muted-foreground/25" />
58 {!compactRows && <span className="h-0.5 w-2 rounded-full bg-muted-foreground/15" />}
59 </span>
60 </span>
61 ))}
62 </span>
63 <span aria-hidden className="flex min-w-0 flex-1 flex-col gap-1 px-1 pt-1">
64 <span className="h-0.5 w-full rounded-full bg-muted-foreground/25" />
65 <span className="h-0.5 w-4/5 rounded-full bg-muted-foreground/20" />
66 <span className="h-0.5 w-3/5 rounded-full bg-muted-foreground/15" />
67 </span>
68 <span
69 aria-hidden
70 className={cn(
71 "pointer-events-none absolute top-3 left-5 flex w-9 flex-col gap-1 rounded-sm border border-border bg-popover p-1 opacity-0 shadow-xs transition-opacity duration-150 ease-out motion-reduce:transition-none",
72 collapsed &&
73 railEnabled &&
74 "group-hover/compact-preview:opacity-100 group-focus-visible/compact-preview:opacity-100",
75 )}
76 >
77 <span className="h-0.5 w-full rounded-full bg-popover-foreground/45" />
78 <span className="h-0.5 w-2/3 rounded-full bg-popover-foreground/25" />
79 </span>
80 </button>
81 );
82}
apps/web/src/components/settings/SettingsPanels.tsx-80
......@@ -170,7 +170,6 @@ import {
170170import { searchableSetting } from "./settingsSearch";
171171import { ProjectFavicon } from "../ProjectFavicon";
172172import { PanelAnimationsPreview } from "./PanelAnimationsPreview";
173import { CompactSidebarPreview } from "./CompactSidebarPreview";
174173
175174const ENVIRONMENT_IDENTIFICATION_LABELS: Record<EnvironmentIdentificationMode, string> = {
176175 artwork: "Artwork",
......@@ -524,10 +523,6 @@ export function useSettingsRestore(onRestored?: () => void) {
524523 ...(theme !== "system" ? ["Theme"] : []),
525524 ...(!followSystem ? ["Follow system"] : []),
526525 ...(themeHalves !== null ? ["Theme mix"] : []),
527 ...(settings.compactSidebarEnabled !== DEFAULT_UNIFIED_SETTINGS.compactSidebarEnabled ||
528 settings.sidebarCompactThreadRows !== DEFAULT_UNIFIED_SETTINGS.sidebarCompactThreadRows
529 ? ["Compact sidebar"]
530 : []),
531526 ...(settings.appearanceContrast !== DEFAULT_UNIFIED_SETTINGS.appearanceContrast
532527 ? ["Contrast"]
533528 : []),
......@@ -634,7 +629,6 @@ export function useSettingsRestore(onRestored?: () => void) {
634629 settings.browserLinkTarget,
635630 settings.browserAutoShowFloatingPreview,
636631 settings.appearanceContrast,
637 settings.compactSidebarEnabled,
638632 settings.diffColorScheme,
639633 settings.enableAgentBrowserAccess,
640634 settings.confirmQuit,
......@@ -666,7 +660,6 @@ export function useSettingsRestore(onRestored?: () => void) {
666660 settings.continueThreadsAfterServerUpdate,
667661 settings.sidebarAutoSettleAfterDays,
668662 settings.sidebarAutoSettleOnMerge,
669 settings.sidebarCompactThreadRows,
670663 settings.sidebarProjectGroupingMode,
671664 settings.sidebarThreadPreviewCount,
672665 settings.showSkillsInSlashMenu,
......@@ -744,7 +737,6 @@ export function useSettingsRestore(onRestored?: () => void) {
744737 }
745738 updateSettings({
746739 appearanceContrast: DEFAULT_UNIFIED_SETTINGS.appearanceContrast,
747 compactSidebarEnabled: DEFAULT_UNIFIED_SETTINGS.compactSidebarEnabled,
748740 diffColorScheme: DEFAULT_UNIFIED_SETTINGS.diffColorScheme,
749741 timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat,
750742 notificationMode: DEFAULT_UNIFIED_SETTINGS.notificationMode,
......@@ -762,7 +754,6 @@ export function useSettingsRestore(onRestored?: () => void) {
762754 panelAnimationDurationMs: DEFAULT_UNIFIED_SETTINGS.panelAnimationDurationMs,
763755 sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount,
764756 sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode,
765 sidebarCompactThreadRows: DEFAULT_UNIFIED_SETTINGS.sidebarCompactThreadRows,
766757 sidebarAutoSettleAfterDays: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays,
767758 sidebarAutoSettleOnMerge: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleOnMerge,
768759 responseStreamingMode: DEFAULT_UNIFIED_SETTINGS.responseStreamingMode,
......@@ -1143,19 +1134,6 @@ export function AppearanceSettingsPanel() {
11431134 const [isImportThemeOpen, setIsImportThemeOpen] = useState(false);
11441135 const settings = useScopedSettings();
11451136 const updateSettings = useUpdateScopedSettings();
1146 const compactSidebarMode = settings.compactSidebarEnabled
1147 ? settings.sidebarCompactThreadRows
1148 ? "both"
1149 : "rail"
1150 : settings.sidebarCompactThreadRows
1151 ? "threads"
1152 : "off";
1153 const compactSidebarModes = {
1154 off: "Off",
1155 rail: "Rail only",
1156 threads: "Threads only",
1157 both: "Both",
1158 };
11591137 const environmentStageLabel = useEnvironmentStageLabel();
11601138 const showEnvironmentIdentification =
11611139 resolveEnvironmentIdentificationPillLabel(environmentStageLabel) !== null;
......@@ -1442,64 +1420,6 @@ export function AppearanceSettingsPanel() {
14421420 />
14431421 </SettingsSection>
14441422
1445 <SettingsSection id="appearance-sidebar" title="Sidebar">
1446 <SettingsRow
1447 {...searchableSetting("compact-sidebar")}
1448 description="Choose a collapsed icon rail, denser thread rows, or both. Click the preview to collapse or expand."
1449 resetAction={
1450 settings.compactSidebarEnabled !== DEFAULT_UNIFIED_SETTINGS.compactSidebarEnabled ||
1451 settings.sidebarCompactThreadRows !==
1452 DEFAULT_UNIFIED_SETTINGS.sidebarCompactThreadRows ? (
1453 <SettingResetButton
1454 label="compact sidebar"
1455 onClick={() =>
1456 updateSettings({
1457 compactSidebarEnabled: DEFAULT_UNIFIED_SETTINGS.compactSidebarEnabled,
1458 sidebarCompactThreadRows: DEFAULT_UNIFIED_SETTINGS.sidebarCompactThreadRows,
1459 })
1460 }
1461 />
1462 ) : null
1463 }
1464 control={
1465 <div className="grid w-full grid-cols-[5rem_1fr] items-center gap-3 sm:w-auto sm:grid-cols-[7rem_10rem] sm:gap-4">
1466 <CompactSidebarPreview
1467 key={compactSidebarMode}
1468 railEnabled={settings.compactSidebarEnabled}
1469 compactRows={settings.sidebarCompactThreadRows}
1470 />
1471 <Select
1472 value={compactSidebarMode}
1473 onValueChange={(value) => {
1474 if (
1475 value !== "off" &&
1476 value !== "rail" &&
1477 value !== "threads" &&
1478 value !== "both"
1479 )
1480 return;
1481 updateSettings({
1482 compactSidebarEnabled: value === "rail" || value === "both",
1483 sidebarCompactThreadRows: value === "threads" || value === "both",
1484 });
1485 }}
1486 >
1487 <SelectTrigger size="sm" className="w-full" aria-label="Compact sidebar">
1488 <SelectValue>{compactSidebarModes[compactSidebarMode]}</SelectValue>
1489 </SelectTrigger>
1490 <SelectPopup align="end" alignItemWithTrigger={false}>
1491 {Object.entries(compactSidebarModes).map(([value, label]) => (
1492 <SelectItem key={value} hideIndicator value={value}>
1493 {label}
1494 </SelectItem>
1495 ))}
1496 </SelectPopup>
1497 </Select>
1498 </div>
1499 }
1500 />
1501 </SettingsSection>
1502
15031423 <TypographySection />
15041424 </SettingsPageContainer>
15051425 );
apps/web/src/components/settings/SettingsSidebarNav.tsx+7-26
......@@ -24,7 +24,6 @@ import {
2424 XIcon,
2525} from "lucide-react";
2626import { useLocation, useNavigate } from "@tanstack/react-router";
27import { useCompactSidebarEnabled } from "../../hooks/useSettings";
2827
2928import { Button } from "../ui/button";
3029import { Input } from "../ui/input";
......@@ -112,13 +111,12 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
112111 (item) => item.to !== "/settings/projects" || isSettingsOverviewVisible(scopeSearch),
113112 );
114113 const { isMobile, setOpenMobile, open, setOpen } = useSidebar();
115 const compactSidebarEnabled = useCompactSidebarEnabled();
116114 const searchInputRef = useRef<HTMLInputElement>(null);
117115 const [query, setQuery] = useState("");
118116 const [activeResultIndex, setActiveResultIndex] = useState(0);
119117 const searchableItems = useAvailableSettingsSearchItems();
120118 const results = useMemo(() => searchSettings(query, searchableItems), [query, searchableItems]);
121 const isSearching = query.trim().length > 0 && !(compactSidebarEnabled && !isMobile && !open);
119 const isSearching = query.trim().length > 0;
122120 const hasResults = results.length > 0;
123121
124122 useEffect(() => {
......@@ -235,18 +233,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
235233 <>
236234 <SidebarContent className="overflow-x-hidden">
237235 <SidebarGroup className="gap-2 p-[var(--sidebar-content-inset)]">
238 <SidebarMenuButton
239 className="hidden group-data-[collapsible=icon]:flex"
240 aria-label="Search settings"
241 tooltip="Search settings"
242 onClick={() => {
243 setOpen(true);
244 requestAnimationFrame(() => searchInputRef.current?.focus());
245 }}
246 >
247 <SearchIcon />
248 </SidebarMenuButton>
249 <div className="flex h-8 items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-sidebar-muted-foreground hover:bg-sidebar-row-hover hover:text-sidebar-foreground group-data-[collapsible=icon]:hidden">
236 <div className="flex h-8 items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-sidebar-muted-foreground hover:bg-sidebar-row-hover hover:text-sidebar-foreground">
250237 <SearchIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" />
251238 <Input
252239 ref={searchInputRef}
......@@ -343,14 +330,10 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
343330 <SidebarMenuItem key={item.to}>
344331 <SidebarMenuButton
345332 isActive={isActive}
346 aria-label={item.label}
347 tooltip={item.label}
348333 onClick={() => handleSectionClick(item.to)}
349334 >
350335 <Icon />
351 <span className="truncate group-data-[collapsible=icon]:hidden">
352 {item.label}
353 </span>
336 <span className="truncate">{item.label}</span>
354337 </SidebarMenuButton>
355338 </SidebarMenuItem>
356339 );
......@@ -360,12 +343,10 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
360343 </SidebarGroup>
361344 </SidebarContent>
362345 <SidebarFooter className="px-[var(--sidebar-content-inset)] py-1">
363 <div className="contents group-data-[collapsible=icon]:hidden">
364 <Suspense fallback={null}>
365 <T3ConnectSidebarSignIn />
366 </Suspense>
367 </div>
368 <div className="flex items-center gap-1 group-data-[collapsible=icon]:flex-col">
346 <Suspense fallback={null}>
347 <T3ConnectSidebarSignIn />
348 </Suspense>
349 <div className="flex items-center gap-1">
369350 <div className="min-w-0 flex-1">
370351 <SidebarUtilityMenu />
371352 </div>
apps/web/src/components/settings/settingsSearch.ts-8
......@@ -159,14 +159,6 @@ export const SETTINGS_SEARCH_ITEMS = [
159159 title: "Panel animations",
160160 to: "/settings/appearance",
161161 },
162 {
163 id: "compact-sidebar",
164 title: "Compact sidebar",
165 to: "/settings/appearance",
166 searchTerms: [
167 "collapsed icons rail hover navigation preview expanded dense density one line rows chats threads compact thread list",
168 ],
169 },
170162 {
171163 id: "environment-identification",
172164 title: "Environment identification",
apps/web/src/components/sidebar/SidebarChrome.tsx+6-8
......@@ -86,7 +86,7 @@ function SidebarBrand({ onBackdrop }: { onBackdrop: boolean }) {
8686 <Link
8787 aria-label="Go to threads"
8888 className={cn(
89 "relative z-10 ml-[var(--workspace-titlebar-content-left)] hidden h-7 w-fit min-w-0 shrink-0 items-center overflow-hidden rounded-md outline-hidden ring-ring focus-visible:ring-2 md:flex group-data-[collapsible=icon]:hidden",
89 "relative z-10 ml-[var(--workspace-titlebar-content-left)] hidden h-7 w-fit min-w-0 shrink-0 items-center overflow-hidden rounded-md outline-hidden ring-ring focus-visible:ring-2 md:flex",
9090 onBackdrop ? "text-white" : "text-foreground",
9191 )}
9292 to="/"
......@@ -187,12 +187,12 @@ export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() {
187187 }, [canGoBack, closeMobileSidebar, navigate]);
188188
189189 return (
190 <SidebarMenu className="flex-row items-center group-data-[collapsible=icon]:flex-col">
190 <SidebarMenu className="flex-row items-center">
191191 {currentFooterPage ? (
192192 <SidebarMenuItem className="min-w-0 flex-1">
193 <SidebarMenuButton onClick={handleBackClick} aria-label="Back" tooltip="Back">
193 <SidebarMenuButton onClick={handleBackClick}>
194194 <ArrowLeftIcon />
195 <span className="group-data-[collapsible=icon]:hidden">Back</span>
195 <span>Back</span>
196196 </SidebarMenuButton>
197197 </SidebarMenuItem>
198198 ) : (
......@@ -224,10 +224,8 @@ export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() {
224224export const SidebarChromeFooter = memo(function SidebarChromeFooter() {
225225 return (
226226 <SidebarFooter className="px-[var(--sidebar-content-inset)] py-1">
227 <div className="contents group-data-[collapsible=icon]:hidden">
228 <SidebarProviderUpdatePill />
229 <SidebarUpdateArchitectureWarning />
230 </div>
227 <SidebarProviderUpdatePill />
228 <SidebarUpdateArchitectureWarning />
231229 <SidebarUtilityMenu />
232230 </SidebarFooter>
233231 );
apps/web/src/components/sidebar/SidebarCompletedTime.test.tsx deleted-56
......@@ -1,56 +0,0 @@
1import { act, memo } from "react";
2import { create, type ReactTestRenderer } from "react-test-renderer";
3import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
4
5import { SidebarCompletedTime } from "./SidebarCompletedTime";
6
7let renderer: ReactTestRenderer | undefined;
8
9beforeEach(() => {
10 vi.useFakeTimers();
11 vi.setSystemTime(new Date("2026-09-07T01:01:00Z"));
12 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
13 vi.stubGlobal("window", {
14 setTimeout,
15 clearTimeout,
16 setInterval,
17 clearInterval,
18 });
19});
20
21afterEach(async () => {
22 await act(() => renderer?.unmount());
23 renderer = undefined;
24 vi.unstubAllGlobals();
25 vi.useRealTimers();
26});
27
28it("advances visible and accessible completion times without rerendering its memoized row", async () => {
29 const rowRender = vi.fn();
30 const Row = memo(function Row() {
31 rowRender();
32 return <SidebarCompletedTime completedAt="2026-09-07T01:00:00Z" />;
33 });
34 await act(() => {
35 renderer = create(<Row />);
36 });
37 expect(renderer!.root.findByType("time").props.dateTime).toBe("2026-09-07T01:00:00Z");
38 expect(renderer!.root.findByProps({ className: "sr-only" }).children).toEqual(["Completed "]);
39 expect(
40 renderer!.root.findAll((node) => node.props.role === "status" || node.props["aria-live"]),
41 ).toHaveLength(0);
42 expect(renderer!.root.findByProps({ className: "text-secondary-label" }).children).toEqual([
43 "1m",
44 ]);
45
46 await act(() => vi.advanceTimersByTime(60_000));
47
48 expect(renderer!.root.findByProps({ className: "sr-only" }).children).toEqual(["Completed "]);
49 expect(renderer!.root.findByProps({ className: "text-secondary-label" }).children).toEqual([
50 "2m",
51 ]);
52 expect(rowRender).toHaveBeenCalledTimes(1);
53 await act(() => renderer!.unmount());
54 renderer = undefined;
55 expect(vi.getTimerCount()).toBe(0);
56});
apps/web/src/components/sidebar/SidebarCompletedTime.tsx deleted-16
......@@ -1,16 +0,0 @@
1import { useNowMinute } from "../../hooks/useNowMinute";
2import { formatRelativeTimeLabel } from "../../timestampFormat";
3
4export function SidebarCompletedTime({ completedAt }: { completedAt: string }) {
5 // Subscribe inside the label so time advances even when the row is memoized.
6 const nowMinute = useNowMinute();
7 const relativeTime = formatRelativeTimeLabel(completedAt, Date.parse(`${nowMinute}:00Z`));
8 const label = relativeTime === "just now" ? "now" : relativeTime.replace(/ ago$/, "");
9
10 return (
11 <time dateTime={completedAt} className="inline-flex items-center">
12 <span className="sr-only">Completed </span>
13 <span className="text-secondary-label">{label}</span>
14 </time>
15 );
16}
apps/web/src/components/sidebar/SidebarThreadHeader.tsx+4-27
......@@ -20,10 +20,9 @@ import {
2020} from "react";
2121
2222import { cn } from "~/lib/utils";
23import { useCompactSidebarEnabled } from "../../hooks/useSettings";
2423import { Button } from "../ui/button";
2524import { Input } from "../ui/input";
26import { SidebarMenuButton, useSidebar } from "../ui/sidebar";
25import { SidebarMenuButton } from "../ui/sidebar";
2726import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
2827
2928export interface SidebarThreadHeaderProps {
......@@ -70,9 +69,6 @@ export function SidebarThreadHeader({
7069 activeSearchResultIndex,
7170 onClearSearch,
7271}: SidebarThreadHeaderProps) {
73 const compactEnabled = useCompactSidebarEnabled();
74 const { state, isMobile, setOpen } = useSidebar();
75 const compact = compactEnabled && state === "collapsed" && !isMobile;
7672 const resultsVisible = isSearching && searchResultCount > 0;
7773 // Results shrink as the query narrows, so the active index can outrun the
7874 // list; pointing aria-activedescendant at a removed option strands the
......@@ -83,24 +79,10 @@ export function SidebarThreadHeader({
8379 : "New thread";
8480
8581 return (
86 <div className={cn("flex items-center gap-1", compact && "flex-col")}>
87 {compact ? (
88 <SidebarHeaderIconButton
89 label="Search threads"
90 onClick={() => {
91 setOpen(true);
92 requestAnimationFrame(() => searchInputRef.current?.focus());
93 }}
94 >
95 <SearchIcon />
96 </SidebarHeaderIconButton>
97 ) : null}
82 <div className="flex items-center gap-1">
9883 <div
9984 ref={searchFieldRef}
100 className={cn(
101 "flex h-8 min-w-0 flex-1 items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-sidebar-muted-foreground hover:bg-sidebar-row-hover hover:text-sidebar-foreground",
102 compact && "hidden",
103 )}
85 className="flex h-8 min-w-0 flex-1 items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-sidebar-muted-foreground hover:bg-sidebar-row-hover hover:text-sidebar-foreground"
10486 >
10587 <SearchIcon className="size-4 shrink-0 text-[var(--sidebar-icon-color)]" />
10688 <Input
......@@ -142,12 +124,7 @@ export function SidebarThreadHeader({
142124 </div>
143125 {/* Segmented well: the icons read as one control instead of three loose
144126 buttons competing with the search field beside them. */}
145 <div
146 className={cn(
147 "flex shrink-0 items-center rounded-md bg-sidebar-control-surface/60 p-px",
148 compact && "flex-col",
149 )}
150 >
127 <div className="flex shrink-0 items-center rounded-md bg-sidebar-control-surface/60 p-px">
151128 {hasProjects ? (
152129 <>
153130 {projectScope}
apps/web/src/components/sidebar/SidebarUpdatePill.tsx+1-1
......@@ -348,7 +348,7 @@ function SidebarUpdateControl() {
348348 );
349349
350350 return (
351 <SidebarMenuItem className="ml-auto shrink-0 group-data-[collapsible=icon]:hidden">
351 <SidebarMenuItem className="ml-auto shrink-0">
352352 <Popover
353353 handle={releaseNotesPopoverHandle}
354354 onOpenChange={(open, details) => {
apps/web/src/components/ui/sidebar.tsx-3
......@@ -591,11 +591,9 @@ function SidebarSeparator({ className, ...props }: React.ComponentProps<typeof S
591591function SidebarContent({
592592 className,
593593 fixedHeader,
594 fixedFooter,
595594 ...props
596595}: React.ComponentProps<"div"> & {
597596 fixedHeader?: React.ReactNode;
598 fixedFooter?: React.ReactNode;
599597}) {
600598 return (
601599 <>
......@@ -619,7 +617,6 @@ function SidebarContent({
619617 {...props}
620618 />
621619 </ScrollArea>
622 {fixedFooter ? <div className="w-full shrink-0">{fixedFooter}</div> : null}
623620 </>
624621 );
625622}
apps/web/src/hooks/useSettings.ts-7
......@@ -379,13 +379,6 @@ export function useLegacySidebarEnabled(): boolean {
379379 return settingsHydrated && legacySidebarEnabled;
380380}
381381
382/** Keep the default collapsed sidebar until persisted client settings hydrate. */
383export function useCompactSidebarEnabled(): boolean {
384 const settingsHydrated = useClientSettingsHydrated();
385 const compactSidebarEnabled = useClientSettingsValue().compactSidebarEnabled;
386 return settingsHydrated && compactSidebarEnabled;
387}
388
389382/** Read current settings for one environment, merged with client-local preferences. */
390383export function useEnvironmentSettings<T = UnifiedSettings>(
391384 environmentId: EnvironmentId,
apps/web/src/timestampFormat.test.ts-12
......@@ -228,15 +228,3 @@ describe("formatElapsedDurationLabel", () => {
228228 expect(formatElapsedDurationLabel("2026-04-03T12:00:00.000Z")).toBe("4d");
229229 });
230230});
231
232describe("explicit relative-time clock", () => {
233 it("uses the supplied minute instead of the wall clock", () => {
234 const completedAt = "2026-09-07T01:00:00Z";
235 expect(formatRelativeTimeLabel(completedAt, Date.parse("2026-09-07T01:01:00Z"))).toBe("1m ago");
236 expect(formatRelativeTimeLabel(completedAt, Date.parse("2026-09-07T01:02:00Z"))).toBe("2m ago");
237 expect(formatRelativeTime(completedAt, Date.parse("2026-09-07T01:02:00Z"))).toEqual({
238 value: "2m",
239 suffix: "ago",
240 });
241 });
242});
apps/web/src/timestampFormat.ts+4-4
......@@ -196,10 +196,10 @@ export type RelativeTimeState =
196196 | { status: "invalid" }
197197 | { status: "relative"; value: string; suffix: string | null };
198198
199export function formatRelativeTime(isoDate: string, nowMs = Date.now()): RelativeTimeParts | null {
199export function formatRelativeTime(isoDate: string): RelativeTimeParts | null {
200200 const date = parseTimestampDate(isoDate);
201201 if (!date) return null;
202 const diffMs = nowMs - date.getTime();
202 const diffMs = Date.now() - date.getTime();
203203 if (diffMs < 0) return { value: "just now", suffix: null };
204204 const seconds = Math.floor(diffMs / 1000);
205205 if (seconds < 60) return { value: "just now", suffix: null };
......@@ -211,8 +211,8 @@ export function formatRelativeTime(isoDate: string, nowMs = Date.now()): Relativ
211211 return { value: `${days}d`, suffix: "ago" };
212212}
213213
214export function formatRelativeTimeLabel(isoDate: string, nowMs = Date.now()) {
215 const relative = formatRelativeTime(isoDate, nowMs);
214export function formatRelativeTimeLabel(isoDate: string) {
215 const relative = formatRelativeTime(isoDate);
216216 if (!relative) return "";
217217 return relative.suffix ? `${relative.value} ${relative.suffix}` : relative.value;
218218}
apps/web/src/workspaceTitlebar.ts+1-1
......@@ -1,2 +1,2 @@
11export const COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS =
2 "[[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)))]";
2 "[[data-sidebar-state=collapsed]_&]:pl-[var(--workspace-titlebar-content-left)]";
packages/contracts/src/settings.test.ts+9-12
......@@ -482,18 +482,7 @@ describe("ClientSettings environment identification", () => {
482482
483483describe("ClientSettings sidebar", () => {
484484 it("defaults to the current sidebar", () => {
485 const settings = decodeClientSettings({});
486 expect(settings.legacySidebarEnabled).toBe(false);
487 expect(settings.sidebarCompactThreadRows).toBe(false);
488 });
489
490 it("preserves an explicit compact thread row preference", () => {
491 expect(decodeClientSettings({ sidebarCompactThreadRows: true }).sidebarCompactThreadRows).toBe(
492 true,
493 );
494 expect(
495 decodeClientSettingsPatch({ sidebarCompactThreadRows: true }).sidebarCompactThreadRows,
496 ).toBe(true);
485 expect(decodeClientSettings({}).legacySidebarEnabled).toBe(false);
497486 });
498487
499488 it("drops the retired sidebar v2 beta keys, resetting everyone to the default", () => {
......@@ -506,6 +495,14 @@ describe("ClientSettings sidebar", () => {
506495 expect(decoded).not.toHaveProperty("sidebarV2ConfiguredByUser");
507496 });
508497
498 it("drops the retired compact sidebar keys for users who opted in", () => {
499 const stored = { compactSidebarEnabled: true, sidebarCompactThreadRows: true };
500 const decoded = decodeClientSettings(stored);
501 expect(decoded).not.toHaveProperty("compactSidebarEnabled");
502 expect(decoded).not.toHaveProperty("sidebarCompactThreadRows");
503 expect(decodeClientSettingsPatch(stored)).toEqual({});
504 });
505
509506 it("preserves an explicit legacy sidebar opt-in", () => {
510507 expect(decodeClientSettings({ legacySidebarEnabled: true }).legacySidebarEnabled).toBe(true);
511508 expect(decodeClientSettingsPatch({ legacySidebarEnabled: true }).legacySidebarEnabled).toBe(
packages/contracts/src/settings.ts-4
......@@ -437,7 +437,6 @@ export const ClientSettingsSchema = Schema.Struct({
437437 // old keys, so everyone, including prior beta opt-outs, resets to the new
438438 // default sidebar.
439439 legacySidebarEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
440 compactSidebarEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
441440 sidebarProjectGroupingMode: SidebarProjectGroupingMode.pipe(
442441 Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE)),
443442 ),
......@@ -451,7 +450,6 @@ export const ClientSettingsSchema = Schema.Struct({
451450 sidebarThreadSortOrder: SidebarThreadSortOrder.pipe(
452451 Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_SORT_ORDER)),
453452 ),
454 sidebarCompactThreadRows: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
455453 sidebarThreadPreviewCount: SidebarThreadPreviewCount.pipe(
456454 Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_PREVIEW_COUNT)),
457455 ),
......@@ -1504,14 +1502,12 @@ export const ClientSettingsPatch = Schema.Struct({
15041502 proactivePanelsEnabled: Schema.optionalKey(Schema.Boolean),
15051503 showSkillsInSlashMenu: Schema.optionalKey(Schema.Boolean),
15061504 legacySidebarEnabled: Schema.optionalKey(Schema.Boolean),
1507 compactSidebarEnabled: Schema.optionalKey(Schema.Boolean),
15081505 sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode),
15091506 sidebarProjectGroupingOverrides: Schema.optionalKey(
15101507 Schema.Record(TrimmedNonEmptyString, SidebarProjectGroupingMode),
15111508 ),
15121509 sidebarProjectSortOrder: Schema.optionalKey(SidebarProjectSortOrder),
15131510 sidebarThreadSortOrder: Schema.optionalKey(SidebarThreadSortOrder),
1514 sidebarCompactThreadRows: Schema.optionalKey(Schema.Boolean),
15151511 sidebarThreadPreviewCount: Schema.optionalKey(SidebarThreadPreviewCount),
15161512 timestampFormat: Schema.optionalKey(TimestampFormat),
15171513 snapShotEnabled: Schema.optionalKey(Schema.Boolean),