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 = {...@@ -57,7 +57,6 @@ const clientSettings: ClientSettings = {
57 proactivePanelsEnabled: true,57 proactivePanelsEnabled: true,
58 showSkillsInSlashMenu: false,58 showSkillsInSlashMenu: false,
59 providerModelPreferences: {},59 providerModelPreferences: {},
60 sidebarCompactThreadRows: false,
61 sidebarProjectGroupingMode: "repository_path",60 sidebarProjectGroupingMode: "repository_path",
62 sidebarProjectGroupingOverrides: {61 sidebarProjectGroupingOverrides: {
63 "environment-1:/tmp/project-a": "separate",62 "environment-1:/tmp/project-a": "separate",
apps/web/src/components/AppSidebarLayout.tsx+2-7
...@@ -14,11 +14,7 @@ import { getLocalStorageItem, removeLocalStorageItem } from "../hooks/useLocalSt...@@ -14,11 +14,7 @@ import { getLocalStorageItem, removeLocalStorageItem } from "../hooks/useLocalSt
14import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings";14import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings";
15import { cn, isMacPlatform } from "../lib/utils";15import { cn, isMacPlatform } from "../lib/utils";
16import { primaryServerKeybindingsAtom } from "../state/server";16import { primaryServerKeybindingsAtom } from "../state/server";
17import {17import { useEnvironmentIdentificationMode, useLegacySidebarEnabled } from "../hooks/useSettings";
18 useCompactSidebarEnabled,
19 useEnvironmentIdentificationMode,
20 useLegacySidebarEnabled,
21} from "../hooks/useSettings";
22import {18import {
23 PanelAnimationSuppressionProvider,19 PanelAnimationSuppressionProvider,
24 usePanelAnimationSettings,20 usePanelAnimationSettings,
...@@ -148,7 +144,6 @@ function ProjectProjectionRetention() {...@@ -148,7 +144,6 @@ function ProjectProjectionRetention() {
148export function AppSidebarLayout({ children }: { children: ReactNode }) {144export function AppSidebarLayout({ children }: { children: ReactNode }) {
149 const navigate = useNavigate();145 const navigate = useNavigate();
150 const legacySidebarEnabled = useLegacySidebarEnabled();146 const legacySidebarEnabled = useLegacySidebarEnabled();
151 const compactSidebarEnabled = useCompactSidebarEnabled();
152 const { active: panelAnimationsActive, durationMs: panelAnimationDurationMs } =147 const { active: panelAnimationsActive, durationMs: panelAnimationDurationMs } =
153 usePanelAnimationSettings();148 usePanelAnimationSettings();
154 // Settings routes show the settings nav in place of whichever thread149 // Settings routes show the settings nav in place of whichever thread
...@@ -234,7 +229,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {...@@ -234,7 +229,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
234 <ProjectProjectionRetention />229 <ProjectProjectionRetention />
235 <Sidebar230 <Sidebar
236 side="left"231 side="left"
237 collapsible={compactSidebarEnabled ? "icon" : "offcanvas"}232 collapsible="offcanvas"
238 data-app-sidebar=""233 data-app-sidebar=""
239 className="border-r border-sidebar-border bg-sidebar text-sidebar-foreground"234 className="border-r border-sidebar-border bg-sidebar text-sidebar-foreground"
240 resizable={{235 resizable={{
apps/web/src/components/LegacySidebar.tsx+25-52
...@@ -199,11 +199,7 @@ import { SidebarChromeFooter, SidebarChromeHeader } from "./sidebar/SidebarChrom...@@ -199,11 +199,7 @@ import { SidebarChromeFooter, SidebarChromeHeader } from "./sidebar/SidebarChrom
199import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";199import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";
200import { useIsMobile } from "~/hooks/useMediaQuery";200import { useIsMobile } from "~/hooks/useMediaQuery";
201import { CommandDialogTrigger } from "./ui/command";201import { CommandDialogTrigger } from "./ui/command";
202import {202import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings";
203 useClientSettings,
204 useCompactSidebarEnabled,
205 useUpdateClientSettings,
206} from "~/hooks/useSettings";
207import { primaryServerKeybindingsAtom } from "../state/server";203import { primaryServerKeybindingsAtom } from "../state/server";
208import {204import {
209 derivePhysicalProjectKey,205 derivePhysicalProjectKey,
...@@ -1198,9 +1194,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec...@@ -1198,9 +1194,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
1198 const router = useRouter();1194 const router = useRouter();
1199 const queuePendingFileDrop = useSidebarPendingFileDropStore((s) => s.queuePendingFileDrop);1195 const queuePendingFileDrop = useSidebarPendingFileDropStore((s) => s.queuePendingFileDrop);
1200 const clearPendingFileDrop = useSidebarPendingFileDropStore((s) => s.clearPendingFileDrop);1196 const clearPendingFileDrop = useSidebarPendingFileDropStore((s) => s.clearPendingFileDrop);
1201 const { isMobile, setOpenMobile, state, setOpen } = useSidebar();1197 const { isMobile, setOpenMobile } = useSidebar();
1202 const compactSidebarEnabled = useCompactSidebarEnabled();
1203 const isCompact = compactSidebarEnabled && !isMobile && state === "collapsed";
1204 const markThreadUnread = useUiStateStore((state) => state.markThreadUnread);1198 const markThreadUnread = useUiStateStore((state) => state.markThreadUnread);
1205 const setProjectExpanded = useUiStateStore((state) => state.setProjectExpanded);1199 const setProjectExpanded = useUiStateStore((state) => state.setProjectExpanded);
1206 const toggleThreadSelection = useThreadSelectionStore((state) => state.toggleThread);1200 const toggleThreadSelection = useThreadSelectionStore((state) => state.toggleThread);
...@@ -1451,13 +1445,10 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec...@@ -1451,13 +1445,10 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
1451 if (useThreadSelectionStore.getState().hasSelection()) {1445 if (useThreadSelectionStore.getState().hasSelection()) {
1452 clearSelection();1446 clearSelection();
1453 }1447 }
1454 setProjectExpanded(projectPreferenceKeys, isCompact || !projectExpanded);1448 setProjectExpanded(projectPreferenceKeys, !projectExpanded);
1455 if (isCompact) setOpen(true);
1456 },1449 },
1457 [1450 [
1458 clearSelection,1451 clearSelection,
1459 isCompact,
1460 setOpen,
1461 dragInProgressRef,1452 dragInProgressRef,
1462 projectExpanded,1453 projectExpanded,
1463 projectPreferenceKeys,1454 projectPreferenceKeys,
...@@ -1474,17 +1465,9 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec...@@ -1474,17 +1465,9 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
1474 if (dragInProgressRef.current) {1465 if (dragInProgressRef.current) {
1475 return;1466 return;
1476 }1467 }
1477 setProjectExpanded(projectPreferenceKeys, isCompact || !projectExpanded);1468 setProjectExpanded(projectPreferenceKeys, !projectExpanded);
1478 if (isCompact) setOpen(true);
1479 },1469 },
1480 [1470 [dragInProgressRef, projectExpanded, projectPreferenceKeys, setProjectExpanded],
1481 dragInProgressRef,
1482 isCompact,
1483 projectExpanded,
1484 projectPreferenceKeys,
1485 setOpen,
1486 setProjectExpanded,
1487 ],
1488 );1471 );
14891472
1490 const handleProjectButtonPointerDownCapture = useCallback(1473 const handleProjectButtonPointerDownCapture = useCallback(
...@@ -2376,10 +2359,8 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec...@@ -2376,10 +2359,8 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
2376 <>2359 <>
2377 <div className="group/project-header relative">2360 <div className="group/project-header relative">
2378 <SidebarMenuButton2361 <SidebarMenuButton
2379 tooltip={project.displayName}
2380 aria-label={project.displayName}
2381 ref={isManualProjectSorting ? dragHandleProps?.setActivatorNodeRef : undefined}2362 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 ${
2383 isManualProjectSorting ? "cursor-grab active:cursor-grabbing" : ""2364 isManualProjectSorting ? "cursor-grab active:cursor-grabbing" : ""
2384 }`}2365 }`}
2385 {...(isManualProjectSorting && dragHandleProps ? dragHandleProps.attributes : {})}2366 {...(isManualProjectSorting && dragHandleProps ? dragHandleProps.attributes : {})}
...@@ -2389,7 +2370,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec...@@ -2389,7 +2370,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
2389 onKeyDown={handleProjectButtonKeyDown}2370 onKeyDown={handleProjectButtonKeyDown}
2390 onContextMenu={handleProjectButtonContextMenu}2371 onContextMenu={handleProjectButtonContextMenu}
2391 >2372 >
2392 {isCompact ? null : !projectExpanded && projectStatus ? (2373 {!projectExpanded && projectStatus ? (
2393 <Tooltip>2374 <Tooltip>
2394 <TooltipTrigger2375 <TooltipTrigger
2395 render={2376 render={
...@@ -2420,7 +2401,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec...@@ -2420,7 +2401,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
2420 <span className="flex shrink-0">2401 <span className="flex shrink-0">
2421 <ProjectFavicon project={project} />2402 <ProjectFavicon project={project} />
2422 </span>2403 </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">
2424 <span className="truncate text-sm font-medium text-sidebar-foreground/90">2405 <span className="truncate text-sm font-medium text-sidebar-foreground/90">
2425 {project.displayName}2406 {project.displayName}
2426 </span>2407 </span>
...@@ -2434,7 +2415,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec...@@ -2434,7 +2415,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
2434 {/* Environment badge – visible by default, crossfades with the2415 {/* Environment badge – visible by default, crossfades with the
2435 "new thread" button on hover using the same pointer-events +2416 "new thread" button on hover using the same pointer-events +
2436 opacity pattern as the thread row archive/timestamp swap. */}2417 opacity pattern as the thread row archive/timestamp swap. */}
2437 {!isCompact && project.environmentPresence === "remote-only" && (2418 {project.environmentPresence === "remote-only" && (
2438 <Tooltip>2419 <Tooltip>
2439 <TooltipTrigger2420 <TooltipTrigger
2440 render={2421 render={
...@@ -2460,7 +2441,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec...@@ -2460,7 +2441,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
2460 <Tooltip>2441 <Tooltip>
2461 <TooltipTrigger2442 <TooltipTrigger
2462 render={2443 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">
2464 <button2445 <button
2465 type="button"2446 type="button"
2466 aria-label={`Create new thread in ${project.displayName}`}2447 aria-label={`Create new thread in ${project.displayName}`}
...@@ -2693,7 +2674,7 @@ function LocalSecondaryStatus() {...@@ -2693,7 +2674,7 @@ function LocalSecondaryStatus() {
2693 }2674 }
26942675
2695 return (2676 return (
2696 <SidebarGroup className="px-2 pt-2 pb-0 group-data-[collapsible=icon]:hidden">2677 <SidebarGroup className="px-2 pt-2 pb-0">
2697 {connecting.length > 0 ? (2678 {connecting.length > 0 ? (
2698 <Alert2679 <Alert
2699 variant="default"2680 variant="default"
...@@ -2997,16 +2978,14 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(...@@ -2997,16 +2978,14 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
2997 render={2978 render={
2998 <SidebarMenuButton2979 <SidebarMenuButton
2999 className="focus-visible:ring-0"2980 className="focus-visible:ring-0"
3000 tooltip="Search"
3001 aria-label="Search"
3002 data-testid="command-palette-trigger"2981 data-testid="command-palette-trigger"
3003 />2982 />
3004 }2983 }
3005 >2984 >
3006 <SearchIcon />2985 <SearchIcon />
3007 <span className="flex-1 truncate group-data-[collapsible=icon]:hidden">Search</span>2986 <span className="flex-1 truncate">Search</span>
3008 {commandPaletteShortcutLabel ? (2987 {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]">
3010 {commandPaletteShortcutLabel}2989 {commandPaletteShortcutLabel}
3011 </Kbd>2990 </Kbd>
3012 ) : null}2991 ) : null}
...@@ -3017,7 +2996,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(...@@ -3017,7 +2996,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
3017 }2996 }
3018 >2997 >
3019 {showArm64IntelBuildWarning && arm64IntelBuildWarningDescription ? (2998 {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">
3021 <Alert variant="warning" className="rounded-2xl border-warning/40 bg-warning/8">3000 <Alert variant="warning" className="rounded-2xl border-warning/40 bg-warning/8">
3022 <TriangleAlertIcon />3001 <TriangleAlertIcon />
3023 <AlertTitle>Intel build on Apple Silicon</AlertTitle>3002 <AlertTitle>Intel build on Apple Silicon</AlertTitle>
...@@ -3041,21 +3020,17 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(...@@ -3041,21 +3020,17 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
3041 ) : null}3020 ) : null}
3042 <LocalSecondaryStatus />3021 <LocalSecondaryStatus />
3043 <SidebarGroup className="px-2 py-2">3022 <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">3023 <div className="mb-1 flex items-center justify-between pl-2 pr-1.5">
3045 <span className="text-xs font-medium text-sidebar-muted-foreground/80 group-data-[collapsible=icon]:hidden">3024 <span className="text-xs font-medium text-sidebar-muted-foreground/80">Projects</span>
3046 Projects
3047 </span>
3048 <div className="flex items-center gap-1">3025 <div className="flex items-center gap-1">
3049 <div className="group-data-[collapsible=icon]:hidden">3026 <ProjectSortMenu
3050 <ProjectSortMenu3027 projectSortOrder={projectSortOrder}
3051 projectSortOrder={projectSortOrder}3028 threadSortOrder={threadSortOrder}
3052 threadSortOrder={threadSortOrder}3029 threadPreviewCount={threadPreviewCount}
3053 threadPreviewCount={threadPreviewCount}3030 onProjectSortOrderChange={handleProjectSortOrderChange}
3054 onProjectSortOrderChange={handleProjectSortOrderChange}3031 onThreadSortOrderChange={handleThreadSortOrderChange}
3055 onThreadSortOrderChange={handleThreadSortOrderChange}3032 onThreadPreviewCountChange={handleThreadPreviewCountChange}
3056 onThreadPreviewCountChange={handleThreadPreviewCountChange}3033 />
3057 />
3058 </div>
3059 <Tooltip>3034 <Tooltip>
3060 <TooltipTrigger3035 <TooltipTrigger
3061 render={3036 render={
...@@ -3152,9 +3127,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(...@@ -3152,9 +3127,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
3152 )}3127 )}
31533128
3154 {projectsLength === 0 && (3129 {projectsLength === 0 && (
3155 <div className="px-2 pt-4 text-center text-secondary-label text-xs group-data-[collapsible=icon]:hidden">3130 <div className="px-2 pt-4 text-center text-secondary-label text-xs">No projects yet</div>
3156 No projects yet
3157 </div>
3158 )}3131 )}
3159 </SidebarGroup>3132 </SidebarGroup>
3160 </SidebarContent>3133 </SidebarContent>
apps/web/src/components/Sidebar.drag.ts+6-14
...@@ -103,7 +103,6 @@ export function createSidebarSortingStrategy(input: {...@@ -103,7 +103,6 @@ export function createSidebarSortingStrategy(input: {
103 snoozedThreadCount?: number;103 snoozedThreadCount?: number;
104 cardHeight?: number;104 cardHeight?: number;
105 slimHeight?: number;105 slimHeight?: number;
106 compact?: boolean;
107 /** Space each pinned boundary opens for its label while dragging. The106 /** Space each pinned boundary opens for its label while dragging. The
108 * markers stay zero height at rest, so nothing is reserved until pickup. */107 * markers stay zero height at rest, so nothing is reserved until pickup. */
109 boundaryLabelHeight?: number;108 boundaryLabelHeight?: number;
...@@ -141,14 +140,11 @@ export function createSidebarSortingStrategy(input: {...@@ -141,14 +140,11 @@ export function createSidebarSortingStrategy(input: {
141 else slimHeight ??= rects[index]?.height;140 else slimHeight ??= rects[index]?.height;
142 if (item.key !== active.key) groups[item.section].push(item);141 if (item.key !== active.key) groups[item.section].push(item);
143 }142 }
144 // Compact icons use h-7; expanded cards include their vertical padding.143 // Cards are 4.875rem + 0.25rem padding; slim rows/placeholders are h-9.
145 const scale = input.compact144 const scale =
146 ? (cardHeight ?? slimHeight ?? 28) / 28145 slimHeight !== undefined ? slimHeight / 36 : (headerScale ?? (cardHeight ?? 82) / 82);
147 : slimHeight !== undefined146 cardHeight ??= 82 * scale;
148 ? slimHeight / 36147 slimHeight ??= 36 * scale;
149 : (headerScale ?? (cardHeight ?? 82) / 82);
150 cardHeight ??= (input.compact ? 28 : 82) * scale;
151 slimHeight ??= (input.compact ? 28 : 36) * scale;
152 const labelHeight = (input.boundaryLabelHeight ?? 0) * scale;148 const labelHeight = (input.boundaryLabelHeight ?? 0) * scale;
153 const group = groups[target.section];149 const group = groups[target.section];
154 const order =150 const order =
...@@ -222,11 +218,7 @@ export function createSidebarSortingStrategy(input: {...@@ -222,11 +218,7 @@ export function createSidebarSortingStrategy(input: {
222 // Consume the shelf's auto margin as drag labels and resized rows need218 // Consume the shelf's auto margin as drag labels and resized rows need
223 // room, keeping the combined shelves at their measured bottom.219 // room, keeping the combined shelves at their measured bottom.
224 let shelfSpace =220 let shelfSpace =
225 !input.compact &&221 shelfRect && beforeShelf && lastRect && shelfRect.top > beforeShelf.bottom + 1
226 shelfRect &&
227 beforeShelf &&
228 lastRect &&
229 shelfRect.top > beforeShelf.bottom + 1
230 ? Math.max(222 ? Math.max(
231 0,223 0,
232 lastRect.bottom - rects[0].top - heights.reduce((sum, height) => sum + height + 1, -1),224 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 @@...@@ -1,12 +1,10 @@
1import { useSupportsMultiplePullRequests } from "~/hooks/useSupportsMultiplePullRequests";1import { useSupportsMultiplePullRequests } from "~/hooks/useSupportsMultiplePullRequests";
2import { useCompactSidebarEnabled } from "../hooks/useSettings";
3import { resolveThreadCurrentPullRequestLink } from "@t3tools/shared/threadPullRequests";2import { resolveThreadCurrentPullRequestLink } from "@t3tools/shared/threadPullRequests";
4import { useAtomValue } from "@effect/atom-react";3import { useAtomValue } from "@effect/atom-react";
5import { replaceComposerContextReferences } from "@t3tools/shared/composerContextReferences";4import { replaceComposerContextReferences } from "@t3tools/shared/composerContextReferences";
6import * as Schema from "effect/Schema";5import * as Schema from "effect/Schema";
7import {6import {
8 DndContext,7 DndContext,
9 DragOverlay,
10 useSensor,8 useSensor,
11 useSensors,9 useSensors,
12 type DragEndEvent,10 type DragEndEvent,
...@@ -74,7 +72,6 @@ import {...@@ -74,7 +72,6 @@ import {
74 type MouseEvent as ReactMouseEvent,72 type MouseEvent as ReactMouseEvent,
75 type ReactNode,73 type ReactNode,
76} from "react";74} from "react";
77import { createPortal } from "react-dom";
78import { useParams, useRouter } from "@tanstack/react-router";75import { useParams, useRouter } from "@tanstack/react-router";
7976
80import { useRightPanelStore } from "../rightPanelStore";77import { useRightPanelStore } from "../rightPanelStore";
...@@ -124,7 +121,6 @@ import { startNewThreadFromContext } from "../lib/chatThreadActions";...@@ -124,7 +121,6 @@ import { startNewThreadFromContext } from "../lib/chatThreadActions";
124import { useClientSettings } from "../hooks/useSettings";121import { useClientSettings } from "../hooks/useSettings";
125import { useCopyToClipboard } from "../hooks/useCopyToClipboard";122import { useCopyToClipboard } from "../hooks/useCopyToClipboard";
126import { useLocalStorage } from "../hooks/useLocalStorage";123import { useLocalStorage } from "../hooks/useLocalStorage";
127import { SidebarCompletedTime } from "./sidebar/SidebarCompletedTime";
128import { useNowMinute } from "../hooks/useNowMinute";124import { useNowMinute } from "../hooks/useNowMinute";
129import { useEnvironments, usePrimaryEnvironmentId } from "../state/environments";125import { useEnvironments, usePrimaryEnvironmentId } from "../state/environments";
130import {126import {
...@@ -325,7 +321,6 @@ function SidebarThreadTooltip({...@@ -325,7 +321,6 @@ function SidebarThreadTooltip({
325 branchMismatch,321 branchMismatch,
326 terminalStatus,322 terminalStatus,
327 terminalProcessCount,323 terminalProcessCount,
328 compactStatus,
329}: {324}: {
330 thread: SidebarThreadSummary;325 thread: SidebarThreadSummary;
331 project: ProjectFaviconProject | null;326 project: ProjectFaviconProject | null;
...@@ -342,7 +337,6 @@ function SidebarThreadTooltip({...@@ -342,7 +337,6 @@ function SidebarThreadTooltip({
342 } | null;337 } | null;
343 terminalStatus: TerminalStatusIndicator | null;338 terminalStatus: TerminalStatusIndicator | null;
344 terminalProcessCount: number;339 terminalProcessCount: number;
345 compactStatus?: string | undefined;
346}) {340}) {
347 const driverKind = providerEntry?.driverKind ?? null;341 const driverKind = providerEntry?.driverKind ?? null;
348 const supportsMultiplePullRequests = useSupportsMultiplePullRequests(thread.environmentId);342 const supportsMultiplePullRequests = useSupportsMultiplePullRequests(thread.environmentId);
...@@ -359,7 +353,6 @@ function SidebarThreadTooltip({...@@ -359,7 +353,6 @@ function SidebarThreadTooltip({
359 {thread.title}353 {thread.title}
360 </div>354 </div>
361 <div className="grid gap-1.5 pl-0.5 text-xs text-muted-foreground">355 <div className="grid gap-1.5 pl-0.5 text-xs text-muted-foreground">
362 {compactStatus ? <div>{compactStatus}</div> : null}
363 {projectDisplayName ? (356 {projectDisplayName ? (
364 <div className="flex min-w-0 items-center gap-2">357 <div className="flex min-w-0 items-center gap-2">
365 {project ? <ProjectFavicon project={project} className="size-3 shrink-0" /> : null}358 {project ? <ProjectFavicon project={project} className="size-3 shrink-0" /> : null}
...@@ -509,7 +502,7 @@ function SnoozePopoverButton(props: {...@@ -509,7 +502,7 @@ function SnoozePopoverButton(props: {
509type SortableThreadRowBag = Pick<502type SortableThreadRowBag = Pick<
510 ReturnType<typeof useSortable>,503 ReturnType<typeof useSortable>,
511 "listeners" | "setNodeRef" | "transform" | "transition" | "isDragging"504 "listeners" | "setNodeRef" | "transform" | "transition" | "isDragging"
512> & { hidden?: boolean };505>;
513506
514function SortableThreadRow(props: {507function SortableThreadRow(props: {
515 id: string;508 id: string;
...@@ -590,18 +583,7 @@ function SidebarSectionPlaceholder(props: {...@@ -590,18 +583,7 @@ function SidebarSectionPlaceholder(props: {
590 props.isDropTarget && "border-primary/40 bg-primary/5 text-primary",583 props.isDropTarget && "border-primary/40 bg-primary/5 text-primary",
591 )}584 )}
592 >585 >
593 <span className="group-data-[collapsible=icon]:sr-only">{props.label}</span>586 {props.label}
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 )}
605 </div>587 </div>
606 ) : null}588 ) : null}
607 </SortableSidebarMarker>589 </SortableSidebarMarker>
...@@ -625,30 +607,19 @@ function SidebarDragBoundary(props: {...@@ -625,30 +607,19 @@ function SidebarDragBoundary(props: {
625 className="pointer-events-none relative mx-0.5 -mb-px h-0"607 className="pointer-events-none relative mx-0.5 -mb-px h-0"
626 >608 >
627 {props.visible ? (609 {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">
629 <span611 <span
630 className={cn(612 className={cn(
631 "shrink-0 text-xs font-medium",613 "shrink-0 text-xs font-medium",
632 props.isDropTarget ? "text-primary" : "text-sidebar-foreground/80",614 props.isDropTarget ? "text-primary" : "text-sidebar-foreground/80",
633 )}615 )}
634 >616 >
635 <span className="group-data-[collapsible=icon]:sr-only">{props.label}</span>617 {props.label}
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 )}
647 </span>618 </span>
648 <span619 <span
649 aria-hidden620 aria-hidden
650 className={cn(621 className={cn(
651 "h-px flex-1 group-data-[collapsible=icon]:hidden",622 "h-px flex-1",
652 props.isDropTarget ? "bg-primary/50" : "bg-sidebar-foreground/25",623 props.isDropTarget ? "bg-primary/50" : "bg-sidebar-foreground/25",
653 )}624 )}
654 />625 />
...@@ -669,32 +640,20 @@ function SidebarSectionHeader(props: {...@@ -669,32 +640,20 @@ function SidebarSectionHeader(props: {
669 isDropTarget?: boolean;640 isDropTarget?: boolean;
670 toggle: { expanded: boolean; onToggle: () => void };641 toggle: { expanded: boolean; onToggle: () => void };
671}) {642}) {
672 const compactEnabled = useCompactSidebarEnabled();
673 const { state, isMobile } = useSidebar();
674 const compact = compactEnabled && state === "collapsed" && !isMobile;
675 const snoozed = props.marker === "snoozed-header";643 const snoozed = props.marker === "snoozed-header";
676 const className = cn(644 const className = cn(
677 "flex h-full w-full items-center gap-2 px-2 text-left text-xs font-medium",645 "flex h-full w-full items-center gap-2 px-2 text-left text-xs font-medium",
678 compact && "justify-center px-0",
679 snoozed ? "text-blue-600 dark:text-blue-400" : "text-sidebar-muted-foreground/60",646 snoozed ? "text-blue-600 dark:text-blue-400" : "text-sidebar-muted-foreground/60",
680 props.dragging && "text-sidebar-foreground/80",647 props.dragging && "text-sidebar-foreground/80",
681 props.isDropTarget && "text-primary",648 props.isDropTarget && "text-primary",
682 );649 );
683 const content = (650 const content = (
684 <>651 <>
685 <span className={compact ? "sr-only" : "shrink-0"}>{props.label}</span>652 <span className="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}
693 <span653 <span
694 aria-hidden654 aria-hidden
695 className={cn(655 className={cn(
696 "h-px min-w-2 flex-1",656 "h-px min-w-2 flex-1",
697 compact && "hidden",
698 snoozed ? "bg-blue-500/20 dark:bg-blue-400/15" : "bg-sidebar-border/60",657 snoozed ? "bg-blue-500/20 dark:bg-blue-400/15" : "bg-sidebar-border/60",
699 props.dragging && "bg-sidebar-foreground/25",658 props.dragging && "bg-sidebar-foreground/25",
700 props.isDropTarget && "bg-primary/50",659 props.isDropTarget && "bg-primary/50",
...@@ -704,7 +663,6 @@ function SidebarSectionHeader(props: {...@@ -704,7 +663,6 @@ function SidebarSectionHeader(props: {
704 aria-hidden663 aria-hidden
705 className={cn(664 className={cn(
706 "size-3 shrink-0 transition-transform",665 "size-3 shrink-0 transition-transform",
707 compact && "hidden",
708 props.toggle.expanded && "rotate-180",666 props.toggle.expanded && "rotate-180",
709 )}667 )}
710 />668 />
...@@ -716,22 +674,15 @@ function SidebarSectionHeader(props: {...@@ -716,22 +674,15 @@ function SidebarSectionHeader(props: {
716 data-testid={`sidebar-${props.marker}`}674 data-testid={`sidebar-${props.marker}`}
717 className={cn("mx-0.5 h-8", props.className)}675 className={cn("mx-0.5 h-8", props.className)}
718 >676 >
719 <Tooltip disabled={!compact}>677 <button
720 <TooltipTrigger678 type="button"
721 render={679 onClick={props.toggle.onToggle}
722 <button680 aria-expanded={props.toggle.expanded}
723 type="button"681 data-testid={`sidebar-${snoozed ? "snoozed" : "settled"}-shelf-toggle`}
724 onClick={props.toggle.onToggle}682 className={cn(className, "cursor-pointer")}
725 aria-expanded={props.toggle.expanded}683 >
726 data-testid={`sidebar-${snoozed ? "snoozed" : "settled"}-shelf-toggle`}684 {content}
727 className={cn(className, "cursor-pointer")}685 </button>
728 />
729 }
730 >
731 {content}
732 </TooltipTrigger>
733 <TooltipPopup side="right">{props.label}</TooltipPopup>
734 </Tooltip>
735 </SortableSidebarMarker>686 </SortableSidebarMarker>
736 );687 );
737}688}
...@@ -754,9 +705,6 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: {...@@ -754,9 +705,6 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: {
754 onDiscard: (draftId: DraftId) => void;705 onDiscard: (draftId: DraftId) => void;
755}) {706}) {
756 const { composer, draftId, onDiscard, onNavigate, session } = props;707 const { composer, draftId, onDiscard, onNavigate, session } = props;
757 const compactEnabled = useCompactSidebarEnabled();
758 const { state, isMobile } = useSidebar();
759 const compact = compactEnabled && state === "collapsed" && !isMobile;
760 const promptPreview =708 const promptPreview =
761 replaceComposerContextReferences(composer.prompt, (occurrence) => occurrence.label)709 replaceComposerContextReferences(composer.prompt, (occurrence) => occurrence.label)
762 .trim()710 .trim()
...@@ -795,34 +743,6 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: {...@@ -795,34 +743,6 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: {
795 },743 },
796 [draftId, onDiscard],744 [draftId, onDiscard],
797 );745 );
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 }
826 return (746 return (
827 <li className="list-none py-0.5">747 <li className="list-none py-0.5">
828 <div748 <div
...@@ -1032,7 +952,6 @@ const dropVerbBadge: Record<SidebarDropVerb, ReactNode> = {...@@ -1032,7 +952,6 @@ const dropVerbBadge: Record<SidebarDropVerb, ReactNode> = {
1032const SidebarThreadRow = memo(function SidebarThreadRow(props: {952const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1033 thread: SidebarThreadSummary;953 thread: SidebarThreadSummary;
1034 variant: "card" | "slim";954 variant: "card" | "slim";
1035 compact: boolean;
1036 // Slim rows are either settled (action: un-settle) or merely quiet955 // Slim rows are either settled (action: un-settle) or merely quiet
1037 // (seen Ready threads — action: settle).956 // (seen Ready threads — action: settle).
1038 variantAction: "settle" | "unsettle" | "unsnooze";957 variantAction: "settle" | "unsettle" | "unsnooze";
...@@ -1113,9 +1032,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1113,9 +1032,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1113 variant,1032 variant,
1114 variantAction,1033 variantAction,
1115 } = props;1034 } = props;
1116 const compactEnabled = useCompactSidebarEnabled();
1117 const { state, isMobile } = useSidebar();
1118 const compact = compactEnabled && state === "collapsed" && !isMobile;
1119 const threadRef = useMemo(1035 const threadRef = useMemo(
1120 () => scopeThreadRef(thread.environmentId, thread.id),1036 () => scopeThreadRef(thread.environmentId, thread.id),
1121 [thread.environmentId, thread.id],1037 [thread.environmentId, thread.id],
...@@ -1293,16 +1209,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1293,16 +1209,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1293 branchMismatch={branchMismatch}1209 branchMismatch={branchMismatch}
1294 terminalStatus={terminalStatus}1210 terminalStatus={terminalStatus}
1295 terminalProcessCount={terminalProcessCount}1211 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 }
1306 />1212 />
1307 );1213 );
13081214
...@@ -1349,7 +1255,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1349,7 +1255,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1349 [isRenaming, onStartRename, thread.title, threadRef],1255 [isRenaming, onStartRename, thread.title, threadRef],
1350 );1256 );
1351 const [isFileDragOver, setIsFileDragOver] = useState(false);1257 const [isFileDragOver, setIsFileDragOver] = useState(false);
1352 const [tooltipOpen, setTooltipOpen] = useState(false);
1353 const fileDropHandlers = useMemo(1258 const fileDropHandlers = useMemo(
1354 () =>1259 () =>
1355 onFileDropThreads1260 onFileDropThreads
...@@ -1512,7 +1417,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1512,7 +1417,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1512 // A zero-height boundary also makes dnd-kit scale the source to1417 // A zero-height boundary also makes dnd-kit scale the source to
1513 // zero. Only projected peers use scaleY as a visibility sentinel.1418 // zero. Only projected peers use scaleY as a visibility sentinel.
1514 visibility:1419 visibility:
1515 sortable.hidden || (!sortable.isDragging && sortable.transform?.scaleY === 0)1420 !sortable.isDragging && sortable.transform?.scaleY === 0
1516 ? ("hidden" as const)1421 ? ("hidden" as const)
1517 : undefined,1422 : undefined,
1518 },1423 },
...@@ -1584,21 +1489,18 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1584,21 +1489,18 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1584 useRightPanelStore.getState().open(threadRef, "pull-requests");1489 useRightPanelStore.getState().open(threadRef, "pull-requests");
1585 if (!props.isActive) onThreadActivate(threadRef);1490 if (!props.isActive) onThreadActivate(threadRef);
1586 }, [onThreadActivate, props.isActive, threadRef]);1491 }, [onThreadActivate, props.isActive, threadRef]);
1587 const renderPrBadge = (iconOnly: boolean, variant: "underline" | "badge" = "underline") =>1492 const prBadge =
1588 prBadgeShape?.kind === "stack" || pr || currentLinkedPr ? (1493 prBadgeShape?.kind === "stack" || pr || currentLinkedPr ? (
1589 <ThreadPullRequestBadgeControl1494 <ThreadPullRequestBadgeControl
1590 variant={variant}1495 variant="underline"
1591 badge={prBadgeShape}1496 badge={prBadgeShape}
1592 number={pr?.number ?? currentLinkedPr?.number}1497 number={pr?.number ?? currentLinkedPr?.number}
1593 url={pr?.url ?? currentLinkedPr?.url}1498 url={pr?.url ?? currentLinkedPr?.url}
1594 status={prStatus}1499 status={prStatus}
1595 iconOnly={iconOnly}
1596 onOpenStack={handlePrStackClick}1500 onOpenStack={handlePrStackClick}
1597 onOpenPullRequest={handlePrClick}1501 onOpenPullRequest={handlePrClick}
1598 />1502 />
1599 ) : null;1503 ) : null;
1600 const hasPrBadge = prBadgeShape?.kind === "stack" || pr !== null || currentLinkedPr !== null;
1601 const prBadge = renderPrBadge(false);
1602 const terminalStatusIcon = terminalStatus ? (1504 const terminalStatusIcon = terminalStatus ? (
1603 <span1505 <span
1604 role="img"1506 role="img"
...@@ -1656,102 +1558,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1656,102 +1558,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1656 )1558 )
1657 ) : null;1559 ) : 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
1755 if (variant === "slim") {1561 if (variant === "slim") {
1756 return (1562 return (
1757 <li1563 <li
...@@ -1764,11 +1570,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1764,11 +1570,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1764 sortable?.isDragging && "relative z-20",1570 sortable?.isDragging && "relative z-20",
1765 )}1571 )}
1766 >1572 >
1767 <Tooltip1573 <Tooltip disabled={sortable?.isDragging}>
1768 open={!sortable?.isDragging && tooltipOpen}
1769 onOpenChange={setTooltipOpen}
1770 disabled={sortable?.isDragging}
1771 >
1772 <TooltipTrigger1574 <TooltipTrigger
1773 render={1575 render={
1774 <div1576 <div
...@@ -1908,22 +1710,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1908,22 +1710,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1908 );1710 );
1909 }1711 }
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;
1927 const diff = latestTurnDiff(thread);1713 const diff = latestTurnDiff(thread);
19281714
1929 return (1715 return (
...@@ -1932,25 +1718,19 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1932,25 +1718,19 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1932 {...sortableRootProps}1718 {...sortableRootProps}
1933 {...(fileDropHandlers ?? {})}1719 {...(fileDropHandlers ?? {})}
1934 className={cn(1720 className={cn(
1935 "list-none [content-visibility:auto]",1721 // Matches the h-[4.875rem] content box; the py-0.5 padding is added on top.
1936 compactRows1722 "list-none py-0.5 [content-visibility:auto] [contain-intrinsic-size:auto_78px]",
1937 ? "[contain-intrinsic-size:auto_36px]"
1938 : "py-0.5 [contain-intrinsic-size:auto_78px]",
1939 sortable?.isDragging && "relative z-20",1723 sortable?.isDragging && "relative z-20",
1940 )}1724 )}
1941 >1725 >
1942 <Tooltip1726 <Tooltip disabled={snoozeMenuOpen || sortable?.isDragging}>
1943 open={!snoozeMenuOpen && !sortable?.isDragging && tooltipOpen}
1944 onOpenChange={setTooltipOpen}
1945 disabled={snoozeMenuOpen || sortable?.isDragging}
1946 >
1947 <TooltipTrigger1727 <TooltipTrigger
1948 render={1728 render={
1949 <div1729 <div
1950 ref={rowRef}1730 ref={rowRef}
1951 role="button"1731 role="button"
1952 tabIndex={0}1732 tabIndex={0}
1953 data-testid={compactRows ? "sidebar-row-compact" : "sidebar-row-card"}1733 data-testid="sidebar-row-card"
1954 aria-busy={isRegeneratingTitle || undefined}1734 aria-busy={isRegeneratingTitle || undefined}
1955 className={rowSurfaceClassName}1735 className={rowSurfaceClassName}
1956 onClick={handleClick}1736 onClick={handleClick}
...@@ -1960,62 +1740,13 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1960,62 +1740,13 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1960 />1740 />
1961 }1741 }
1962 >1742 >
1963 <div1743 <div className="relative z-10 h-[4.875rem] px-[var(--sidebar-row-content-inset)] py-[var(--sidebar-content-inset)]">
1964 className={cn(1744 <div className="flex h-5 min-w-0 items-center gap-1.5">
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">
1972 {draftIndicator}1745 {draftIndicator}
1973 {props.project ? (1746 {props.project ? (
1974 <span className="relative inline-flex size-4 shrink-0 items-center justify-center">1747 <ProjectFavicon project={props.project} className="size-4 shrink-0" />
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>
2015 ) : null}1748 ) : null}
2016 {compactRows ? (1749 {props.projectDisplayName ? (
2017 title
2018 ) : props.projectDisplayName ? (
2019 <span1750 <span
2020 className={cn(1751 className={cn(
2021 "min-w-0 flex-1 truncate text-secondary-label text-xs",1752 "min-w-0 flex-1 truncate text-secondary-label text-xs",
...@@ -2028,38 +1759,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -2028,38 +1759,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
2028 <span className="flex-1" />1759 <span className="flex-1" />
2029 )}1760 )}
2030 {pinIndicator}1761 {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}
2063 {/* The visible state owns this slot's width: status at rest,1762 {/* The visible state owns this slot's width: status at rest,
2064 actions on hover/keyboard focus or while the popover is open. Keeping1763 actions on hover/keyboard focus or while the popover is open. Keeping
2065 the hidden state out of flow lets the project label reclaim1764 the hidden state out of flow lets the project label reclaim
...@@ -2067,35 +1766,20 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -2067,35 +1766,20 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
2067 {sortable?.isDragging ? (1766 {sortable?.isDragging ? (
2068 dragDestination1767 dragDestination
2069 ) : (1768 ) : (
2070 <span1769 <span className="group/sidebar-status-slot relative ml-auto flex h-5 min-w-8 shrink-0 items-stretch justify-end text-xs">
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 >
2076 {/* Read-only status labels yield to the hover actions. Woke is1770 {/* Read-only status labels yield to the hover actions. Woke is
2077 itself an action, so it stays pointer-enabled and visible1771 itself an action, so it stays pointer-enabled and visible
2078 while the other controls appear beside it. */}1772 while the other controls appear beside it. */}
2079 <span1773 <span
2080 className={cn(1774 className={cn(
2081 isWokeStatus && !compactRows1775 isWokeStatus
2082 ? "pointer-events-auto"1776 ? "pointer-events-auto"
2083 : "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",1777 : "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",
2084 "flex items-center self-center justify-self-end tabular-nums text-secondary-label transition-opacity",1778 "flex items-center self-center justify-self-end tabular-nums text-secondary-label transition-opacity",
2085 snoozeMenuOpen && "pointer-events-none absolute right-0 opacity-0",1779 snoozeMenuOpen && "pointer-events-none absolute right-0 opacity-0",
2086 )}1780 )}
2087 >1781 >
2088 {compactRows ? (1782 {topStatus ? (
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 ? (
2099 isWokeStatus ? (1783 isWokeStatus ? (
2100 <Tooltip>1784 <Tooltip>
2101 <TooltipTrigger1785 <TooltipTrigger
...@@ -2201,7 +1885,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -2201,7 +1885,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
2201 }1885 }
2202 >1886 >
2203 <CheckIcon className="size-3.5" />1887 <CheckIcon className="size-3.5" />
2204 {compactRows ? null : "Settle"}1888 Settle
2205 </TooltipTrigger>1889 </TooltipTrigger>
2206 <TooltipPopup>Settle thread</TooltipPopup>1890 <TooltipPopup>Settle thread</TooltipPopup>
2207 </Tooltip>1891 </Tooltip>
...@@ -2211,70 +1895,68 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -2211,70 +1895,68 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
2211 </span>1895 </span>
2212 )}1896 )}
2213 </div>1897 </div>
2214 {isRegeneratingTitle ? (1898 <div className="mt-1 flex min-w-0">
2215 <span role="status" className="sr-only">1899 {title}
2216 Regenerating title1900 {isRegeneratingTitle ? (
2217 </span>1901 <span role="status" className="sr-only">
2218 ) : null}1902 Regenerating title
2219 {compactRows ? null : (1903 </span>
2220 <>1904 ) : null}
2221 <div className="mt-1 flex min-w-0">{title}</div>1905 </div>
2222 <div className="mt-0.5 flex min-w-0 items-center gap-1.5 text-secondary-label text-xs">1906 <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 while1907 {/* Always the branch. The plan step used to take this slot while
2224 working, but it truncated to a half-sentence and dropped the1908 working, but it truncated to a half-sentence and dropped the
2225 branch, so the row lost its most stable identifier. */}1909 branch, so the row lost its most stable identifier. */}
2226 {thread.branch ? (1910 {thread.branch ? (
2227 <>1911 <>
2228 <ThreadWorktreeIndicator thread={thread} />1912 <ThreadWorktreeIndicator thread={thread} />
2229 <span className="min-w-0 flex-1 truncate whitespace-nowrap text-muted-foreground/40">1913 <span className="min-w-0 flex-1 truncate whitespace-nowrap text-muted-foreground/40">
2230 {thread.branch}1914 {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}
2274 </span>1915 </span>
2275 </div>1916 </>
2276 </>1917 ) : (
2277 )}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>
2278 </div>1960 </div>
2279 {props.jumpLabel ? <JumpHintBadge label={props.jumpLabel} /> : null}1961 {props.jumpLabel ? <JumpHintBadge label={props.jumpLabel} /> : null}
2280 </TooltipTrigger>1962 </TooltipTrigger>
...@@ -2308,9 +1990,6 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {...@@ -2308,9 +1990,6 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {
2308 onFileDropThreads: (threadRef: ScopedThreadRef, files: File[]) => void;1990 onFileDropThreads: (threadRef: ScopedThreadRef, files: File[]) => void;
2309}) {1991}) {
2310 const { thread } = props;1992 const { thread } = props;
2311 const compactEnabled = useCompactSidebarEnabled();
2312 const { state, isMobile } = useSidebar();
2313 const compact = compactEnabled && state === "collapsed" && !isMobile;
2314 const threadRef = useMemo(1993 const threadRef = useMemo(
2315 () => scopeThreadRef(thread.environmentId, thread.id),1994 () => scopeThreadRef(thread.environmentId, thread.id),
2316 [thread.environmentId, thread.id],1995 [thread.environmentId, thread.id],
...@@ -2396,7 +2075,6 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {...@@ -2396,7 +2075,6 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {
2396 onClick={props.onSelect}2075 onClick={props.onSelect}
2397 className={cn(2076 className={cn(
2398 "flex h-9 w-full cursor-pointer items-center gap-2.5 rounded-md px-2.5 text-left text-sm outline-none",2077 "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",
2400 props.isHighlighted || props.isRouteActive2078 props.isHighlighted || props.isRouteActive
2401 ? "bg-sidebar-row-active text-sidebar-foreground"2079 ? "bg-sidebar-row-active text-sidebar-foreground"
2402 : "text-sidebar-muted-foreground/75 hover:bg-sidebar-row-hover hover:text-sidebar-foreground",2080 : "text-sidebar-muted-foreground/75 hover:bg-sidebar-row-hover hover:text-sidebar-foreground",
...@@ -2408,15 +2086,9 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {...@@ -2408,15 +2086,9 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {
2408 >2086 >
2409 {props.project ? (2087 {props.project ? (
2410 <ProjectFavicon project={props.project} className="size-4 shrink-0" />2088 <ProjectFavicon project={props.project} className="size-4 shrink-0" />
2411 ) : compact ? (
2412 <SquarePenIcon aria-hidden className="size-4" />
2413 ) : null}2089 ) : null}
2414 <span className={compact ? "sr-only" : "min-w-0 flex-1 truncate"}>{thread.title}</span>2090 <span className="min-w-0 flex-1 truncate">{thread.title}</span>
2415 <span2091 <span className="shrink-0 text-xs text-muted-foreground/55 tabular-nums">
2416 className={
2417 compact ? "sr-only" : "shrink-0 text-xs text-muted-foreground/55 tabular-nums"
2418 }
2419 >
2420 {threadTimeLabel(thread)}2092 {threadTimeLabel(thread)}
2421 </span>2093 </span>
2422 </TooltipTrigger>2094 </TooltipTrigger>
...@@ -2444,12 +2116,8 @@ export default function Sidebar() {...@@ -2444,12 +2116,8 @@ export default function Sidebar() {
2444 const projectOrder = useUiStateStore((store) => store.projectOrder);2116 const projectOrder = useUiStateStore((store) => store.projectOrder);
2445 const threads = useThreadShells();2117 const threads = useThreadShells();
2446 const router = useRouter();2118 const router = useRouter();
2447 const { isMobile, setOpenMobile, setOpen, state: sidebarState } = useSidebar();2119 const { isMobile, setOpenMobile } = useSidebar();
2448 const compactEnabled = useCompactSidebarEnabled();
2449 const compact = compactEnabled && sidebarState === "collapsed" && !isMobile;
2450 const [snoozedFooter, setSnoozedFooter] = useState<HTMLUListElement | null>(null);
2451 const keybindings = useAtomValue(primaryServerKeybindingsAtom);2120 const keybindings = useAtomValue(primaryServerKeybindingsAtom);
2452 const compactThreadRows = useClientSettings((s) => s.sidebarCompactThreadRows);
2453 const confirmThreadDelete = useClientSettings((s) => s.confirmThreadDelete);2121 const confirmThreadDelete = useClientSettings((s) => s.confirmThreadDelete);
2454 const confirmThreadArchive = useClientSettings((s) => s.confirmThreadArchive);2122 const confirmThreadArchive = useClientSettings((s) => s.confirmThreadArchive);
2455 const sidebarProjectSortOrder = useClientSettings((s) => s.sidebarProjectSortOrder);2123 const sidebarProjectSortOrder = useClientSettings((s) => s.sidebarProjectSortOrder);
...@@ -3020,7 +2688,6 @@ export default function Sidebar() {...@@ -3020,7 +2688,6 @@ export default function Sidebar() {
3020 [setSettledShelfExpanded],2688 [setSettledShelfExpanded],
3021 );2689 );
3022 const renderedSettledThreads = useMemo(() => {2690 const renderedSettledThreads = useMemo(() => {
3023 if (compact) return EMPTY_THREADS;
3024 if (settledShelfExpanded) return visibleSettledThreads;2691 if (settledShelfExpanded) return visibleSettledThreads;
3025 if (routeThreadKey === null) return EMPTY_THREADS;2692 if (routeThreadKey === null) return EMPTY_THREADS;
3026 const routeThread = visibleSettledThreads.find(2693 const routeThread = visibleSettledThreads.find(
...@@ -3028,7 +2695,7 @@ export default function Sidebar() {...@@ -3028,7 +2695,7 @@ export default function Sidebar() {
3028 scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)) === routeThreadKey,2695 scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)) === routeThreadKey,
3029 );2696 );
3030 return routeThread === undefined ? EMPTY_THREADS : [routeThread];2697 return routeThread === undefined ? EMPTY_THREADS : [routeThread];
3031 }, [compact, routeThreadKey, settledShelfExpanded, visibleSettledThreads]);2698 }, [routeThreadKey, settledShelfExpanded, visibleSettledThreads]);
30322699
3033 // The snoozed shelf is collapsed by default: out of the way, never gone.2700 // The snoozed shelf is collapsed by default: out of the way, never gone.
3034 // Collapsed threads don't render (and so don't participate in jump2701 // Collapsed threads don't render (and so don't participate in jump
...@@ -3256,14 +2923,10 @@ export default function Sidebar() {...@@ -3256,14 +2923,10 @@ export default function Sidebar() {
32562923
3257 const [renamingThreadKey, setRenamingThreadKey] = useState<string | null>(null);2924 const [renamingThreadKey, setRenamingThreadKey] = useState<string | null>(null);
3258 const [renamingTitle, setRenamingTitle] = useState("");2925 const [renamingTitle, setRenamingTitle] = useState("");
3259 const startThreadRename = useCallback(2926 const startThreadRename = useCallback((threadRef: ScopedThreadRef, title: string) => {
3260 (threadRef: ScopedThreadRef, title: string) => {2927 setRenamingThreadKey(scopedThreadKey(threadRef));
3261 if (compact) setOpen(true);2928 setRenamingTitle(title);
3262 setRenamingThreadKey(scopedThreadKey(threadRef));2929 }, []);
3263 setRenamingTitle(title);
3264 },
3265 [compact, setOpen],
3266 );
3267 const cancelThreadRename = useCallback(() => setRenamingThreadKey(null), []);2930 const cancelThreadRename = useCallback(() => setRenamingThreadKey(null), []);
3268 const commitThreadRename = useCallback(2931 const commitThreadRename = useCallback(
3269 (threadRef: ScopedThreadRef, title: string, originalTitle: string) => {2932 (threadRef: ScopedThreadRef, title: string, originalTitle: string) => {
...@@ -3425,18 +3088,8 @@ export default function Sidebar() {...@@ -3425,18 +3088,8 @@ export default function Sidebar() {
3425 const threadListRef = useRef<HTMLUListElement | null>(null);3088 const threadListRef = useRef<HTMLUListElement | null>(null);
3426 const dragLabelOffsetRef = useRef(0);3089 const dragLabelOffsetRef = useRef(0);
3427 const restrictBelowPins = useCallback<Modifier>(3090 const restrictBelowPins = useCallback<Modifier>(
3428 (args) =>3091 (args) => restrictBelowSidebarLabel(args, dragLabelOffsetRef.current),
3429 restrictBelowSidebarLabel(3092 [],
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],
3440 );3093 );
3441 const listMotionRef = useRef<ReturnType<typeof createSidebarListMotion> | null>(null);3094 const listMotionRef = useRef<ReturnType<typeof createSidebarListMotion> | null>(null);
3442 const attachListMotionRef = useCallback((node: HTMLUListElement | null) => {3095 const attachListMotionRef = useCallback((node: HTMLUListElement | null) => {
...@@ -3658,7 +3311,7 @@ export default function Sidebar() {...@@ -3658,7 +3311,7 @@ export default function Sidebar() {
3658 pinnedThreads.length +3311 pinnedThreads.length +
3659 activeThreads.length +3312 activeThreads.length +
3660 snoozedThreads.length +3313 snoozedThreads.length +
3661 (compact ? 0 : settledThreads.length) ===3314 settledThreads.length ===
3662 03315 0
3663 ) {3316 ) {
3664 return [];3317 return [];
...@@ -3674,15 +3327,13 @@ export default function Sidebar() {...@@ -3674,15 +3327,13 @@ export default function Sidebar() {
3674 items.push({ kind: "marker", marker: "snoozed-header" });3327 items.push({ kind: "marker", marker: "snoozed-header" });
3675 items.push(...rowsOf(visibleSnoozedThreads, "snoozed"));3328 items.push(...rowsOf(visibleSnoozedThreads, "snoozed"));
3676 }3329 }
3677 if (!compact) {3330 items.push({ kind: "marker", marker: "settled-header" });
3678 items.push({ kind: "marker", marker: "settled-header" });3331 const settledRows = rowsOf(renderedSettledThreads, "settled");
3679 items.push({ kind: "marker", marker: "settled-placeholder" });3332 items.push({ kind: "marker", marker: "settled-placeholder" });
3680 items.push(...rowsOf(renderedSettledThreads, "settled"));3333 items.push(...settledRows);
3681 }
3682 return items;3334 return items;
3683 }, [3335 }, [
3684 activeThreads,3336 activeThreads,
3685 compact,
3686 pinnedThreads,3337 pinnedThreads,
3687 renderedSettledThreads,3338 renderedSettledThreads,
3688 settledThreads.length,3339 settledThreads.length,
...@@ -3752,7 +3403,6 @@ export default function Sidebar() {...@@ -3752,7 +3403,6 @@ export default function Sidebar() {
3752 () =>3403 () =>
3753 createSidebarSortingStrategy({3404 createSidebarSortingStrategy({
3754 items: sidebarListItems,3405 items: sidebarListItems,
3755 compact,
3756 boundaryLabelHeight: SIDEBAR_DRAG_LABEL_HEIGHT,3406 boundaryLabelHeight: SIDEBAR_DRAG_LABEL_HEIGHT,
3757 settledOrder: draggedSettledOrder,3407 settledOrder: draggedSettledOrder,
3758 settledExpanded: settledShelfExpanded,3408 settledExpanded: settledShelfExpanded,
...@@ -3761,7 +3411,6 @@ export default function Sidebar() {...@@ -3761,7 +3411,6 @@ export default function Sidebar() {
3761 snoozedThreadCount: snoozedThreads.length,3411 snoozedThreadCount: snoozedThreads.length,
3762 }),3412 }),
3763 [3413 [
3764 compact,
3765 draggedSettledOrder,3414 draggedSettledOrder,
3766 routeThreadKey,3415 routeThreadKey,
3767 settledShelfExpanded,3416 settledShelfExpanded,
...@@ -3770,22 +3419,6 @@ export default function Sidebar() {...@@ -3770,22 +3419,6 @@ export default function Sidebar() {
3770 snoozedThreads.length,3419 snoozedThreads.length,
3771 ],3420 ],
3772 );3421 );
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 );
3789 // Hidden and filtered threads keep their keys. Reserve those slots without3422 // Hidden and filtered threads keep their keys. Reserve those slots without
3790 // including the rows in the visible drop order or writing to them.3423 // including the rows in the visible drop order or writing to them.
3791 const { pinnedKeysById, activeKeysById } = useMemo(3424 const { pinnedKeysById, activeKeysById } = useMemo(
...@@ -4689,19 +4322,7 @@ export default function Sidebar() {...@@ -4689,19 +4322,7 @@ export default function Sidebar() {
4689 <>4322 <>
4690 <SidebarChromeHeader isElectron={isElectron} />4323 <SidebarChromeHeader isElectron={isElectron} />
4691 <SidebarContent4324 <SidebarContent
4692 className={cn(4325 className="gap-0 min-h-full"
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 }
4705 fixedHeader={4326 fixedHeader={
4706 // Lifted above the stage backdrop, whose fade bleeds below the4327 // Lifted above the stage backdrop, whose fade bleeds below the
4707 // header and would otherwise paint across the search row's outline.4328 // header and would otherwise paint across the search row's outline.
...@@ -4761,12 +4382,8 @@ export default function Sidebar() {...@@ -4761,12 +4382,8 @@ export default function Sidebar() {
4761 // popup opens under the field, is at least as wide as it,4382 // popup opens under the field, is at least as wide as it,
4762 // and grows to fit project names up to a cap, past which4383 // and grows to fit project names up to a cap, past which
4763 // the rows truncate.4384 // the rows truncate.
4764 anchor={compact ? undefined : headerSearchRef}4385 anchor={headerSearchRef}
4765 side={compact ? "right" : "bottom"}4386 className="max-w-[min(18rem,var(--available-width))] overflow-hidden"
4766 className={cn(
4767 "max-w-[min(18rem,var(--available-width))] overflow-hidden",
4768 compact && "min-w-56",
4769 )}
4770 >4387 >
4771 <ComboboxSearchInput4388 <ComboboxSearchInput
4772 aria-label="Search projects"4389 aria-label="Search projects"
...@@ -4868,12 +4485,7 @@ export default function Sidebar() {...@@ -4868,12 +4485,7 @@ export default function Sidebar() {
4868 </SidebarGroup>4485 </SidebarGroup>
4869 }4486 }
4870 >4487 >
4871 <SidebarGroup4488 <SidebarGroup className="ps-[calc(var(--sidebar-content-inset)+1px)] pe-[var(--sidebar-content-inset)] pb-1 pt-0 flex-1">
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 >
4877 {isSearchingThreads ? (4489 {isSearchingThreads ? (
4878 threadSearchResults.length > 0 ? (4490 threadSearchResults.length > 0 ? (
4879 <TooltipProvider4491 <TooltipProvider
...@@ -4926,10 +4538,7 @@ export default function Sidebar() {...@@ -4926,10 +4538,7 @@ export default function Sidebar() {
4926 ) : (4538 ) : (
4927 <p4539 <p
4928 role="status"4540 role="status"
4929 className={cn(4541 className="px-2 py-6 text-center text-xs text-sidebar-muted-foreground"
4930 "px-2 py-6 text-center text-xs text-sidebar-muted-foreground",
4931 compact && "sr-only",
4932 )}
4933 >4542 >
4934 No threads found4543 No threads found
4935 </p>4544 </p>
...@@ -4948,23 +4557,20 @@ export default function Sidebar() {...@@ -4948,23 +4557,20 @@ export default function Sidebar() {
4948 modifiers={[4557 modifiers={[
4949 restrictToVerticalAxis,4558 restrictToVerticalAxis,
4950 restrictBelowPins,4559 restrictBelowPins,
4951 ...(compact ? [] : [restrictToFirstScrollableAncestor]),4560 restrictToFirstScrollableAncestor,
4952 ]}4561 ]}
4953 onDragStart={handleThreadDragStart}4562 onDragStart={handleThreadDragStart}
4954 onDragOver={handleThreadDragOver}4563 onDragOver={handleThreadDragOver}
4955 onDragEnd={handleThreadDragEnd}4564 onDragEnd={handleThreadDragEnd}
4956 >4565 >
4957 <SidebarDragLifecycle onUnmount={cancelThreadDrag} />4566 <SidebarDragLifecycle onUnmount={cancelThreadDrag} />
4958 <SortableContext4567 <SortableContext items={sortableIds} strategy={sidebarSortingStrategy}>
4959 items={sortableIds}
4960 strategy={compact ? compactSidebarSortingStrategy : sidebarSortingStrategy}
4961 >
4962 <ul4568 <ul
4963 ref={attachListMotionRef}4569 ref={attachListMotionRef}
4964 role="list"4570 role="list"
4965 className={cn(4571 className={cn(
4966 "relative flex flex-col gap-px",4572 "relative flex flex-col gap-px",
4967 !compact && sidebarListItems.length > 0 && "flex-1",4573 sidebarListItems.length > 0 && "flex-1",
4968 )}4574 )}
4969 >4575 >
4970 {(() => {4576 {(() => {
...@@ -4976,9 +4582,10 @@ export default function Sidebar() {...@@ -4976,9 +4582,10 @@ export default function Sidebar() {
4976 const threadKey = scopedThreadKey(4582 const threadKey = scopedThreadKey(
4977 scopeThreadRef(thread.environmentId, thread.id),4583 scopeThreadRef(thread.environmentId, thread.id),
4978 );4584 );
4979 // Settled and snoozed always use slim rows. Active and4585 // Settled and snoozed are the ONLY things that collapse a
4980 // pinned threads use cards unless the user has explicitly4586 // row: every other thread is a full card. Density comes
4981 // enabled the compact thread-list preference.4587 // from users (or the auto rules) actually parking work,
4588 // not from the sidebar second-guessing what still matters.
4982 const isCard = section === "active" || section === "pinned";4589 const isCard = section === "active" || section === "pinned";
4983 const rowVariant = isCard ? "card" : "slim";4590 const rowVariant = isCard ? "card" : "slim";
4984 return (4591 return (
...@@ -4988,7 +4595,6 @@ export default function Sidebar() {...@@ -4988,7 +4595,6 @@ export default function Sidebar() {
4988 key={`${threadKey}:${rowVariant}`}4595 key={`${threadKey}:${rowVariant}`}
4989 thread={thread}4596 thread={thread}
4990 variant={rowVariant}4597 variant={rowVariant}
4991 compact={compactThreadRows}
4992 // Snoozed rows wake, settled rows un-settle, and cards settle.4598 // Snoozed rows wake, settled rows un-settle, and cards settle.
4993 variantAction={4599 variantAction={
4994 section === "snoozed"4600 section === "snoozed"
...@@ -5091,25 +4697,11 @@ export default function Sidebar() {...@@ -5091,25 +4697,11 @@ export default function Sidebar() {
5091 !draggableThreadKeys.has(threadKey) || optimisticDrop !== null4697 !draggableThreadKeys.has(threadKey) || optimisticDrop !== null
5092 }4698 }
5093 >4699 >
5094 {(bag) =>4700 {(bag) => renderThreadRowInner(thread, section, bag)}
5095 renderThreadRowInner(
5096 thread,
5097 section,
5098 draggingCompactSnoozed && bag.isDragging
5099 ? { ...bag, hidden: true }
5100 : bag,
5101 )
5102 }
5103 </SortableThreadRow>4701 </SortableThreadRow>
5104 );4702 );
5105 };4703 };
5106 const from = dragState?.activeSection ?? null;4704 const from = dragState?.activeSection ?? null;
5107 const showDragLabels =
5108 from !== null &&
5109 (!compact ||
5110 dragTargetSection === "active" ||
5111 dragTargetSection === "pinned");
5112 const snoozedItems: ReactNode[] = [];
5113 const items: ReactNode[] = [4705 const items: ReactNode[] = [
5114 <SidebarDraftBlock4706 <SidebarDraftBlock
5115 key="draft-sessions"4707 key="draft-sessions"
...@@ -5121,17 +4713,8 @@ export default function Sidebar() {...@@ -5121,17 +4713,8 @@ export default function Sidebar() {
5121 />,4713 />,
5122 ];4714 ];
5123 for (const item of sidebarListItems) {4715 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;
5131 if (item.kind === "thread") {4716 if (item.kind === "thread") {
5132 destination.push(4717 items.push(renderThreadRow(threadByKey.get(item.key)!, item.section));
5133 renderThreadRow(threadByKey.get(item.key)!, item.section),
5134 );
5135 continue;4718 continue;
5136 }4719 }
5137 switch (item.marker) {4720 switch (item.marker) {
...@@ -5141,7 +4724,7 @@ export default function Sidebar() {...@@ -5141,7 +4724,7 @@ export default function Sidebar() {
5141 key="pinned-header"4724 key="pinned-header"
5142 marker="pinned-header"4725 marker="pinned-header"
5143 label="Pinned"4726 label="Pinned"
5144 visible={showDragLabels}4727 visible={from !== null}
5145 isDropTarget={dragTargetSection === "pinned"}4728 isDropTarget={dragTargetSection === "pinned"}
5146 />,4729 />,
5147 );4730 );
...@@ -5152,7 +4735,7 @@ export default function Sidebar() {...@@ -5152,7 +4735,7 @@ export default function Sidebar() {
5152 key="pinned-divider"4735 key="pinned-divider"
5153 marker="pinned-divider"4736 marker="pinned-divider"
5154 label="Active"4737 label="Active"
5155 visible={showDragLabels}4738 visible={from !== null}
5156 isDropTarget={dragTargetSection === "active"}4739 isDropTarget={dragTargetSection === "active"}
5157 />,4740 />,
5158 );4741 );
...@@ -5176,11 +4759,11 @@ export default function Sidebar() {...@@ -5176,11 +4759,11 @@ export default function Sidebar() {
5176 );4759 );
5177 break;4760 break;
5178 case "snoozed-header":4761 case "snoozed-header":
5179 destination.push(4762 items.push(
5180 <SidebarSectionHeader4763 <SidebarSectionHeader
5181 key="snoozed-shelf-header"4764 key="snoozed-shelf-header"
5182 marker="snoozed-header"4765 marker="snoozed-header"
5183 className={cn(!compact && "mt-auto")}4766 className="mt-auto"
5184 label={4767 label={
5185 snoozedShelfExpanded4768 snoozedShelfExpanded
5186 ? "Snoozed"4769 ? "Snoozed"
...@@ -5198,7 +4781,7 @@ export default function Sidebar() {...@@ -5198,7 +4781,7 @@ export default function Sidebar() {
5198 <SidebarSectionHeader4781 <SidebarSectionHeader
5199 key="settled-shelf-header"4782 key="settled-shelf-header"
5200 marker="settled-header"4783 marker="settled-header"
5201 className={cn(!compact && snoozedThreads.length === 0 && "mt-auto")}4784 className={cn(snoozedThreads.length === 0 && "mt-auto")}
5202 label={4785 label={
5203 settledShelfExpanded4786 settledShelfExpanded
5204 ? "Settled"4787 ? "Settled"
...@@ -5233,56 +4816,18 @@ export default function Sidebar() {...@@ -5233,56 +4816,18 @@ export default function Sidebar() {
5233 break;4816 break;
5234 }4817 }
5235 }4818 }
5236 // Keep the shelf inside this drag context while anchoring4819 return items;
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 ];
5261 })()}4820 })()}
5262 {!compact && settledShelfExpanded && hiddenSettledCount > 0 ? (4821 {settledShelfExpanded && hiddenSettledCount > 0 ? (
5263 <li className="list-none">4822 <li className="list-none">
5264 <Tooltip disabled={!compact}>4823 <button
5265 <TooltipTrigger4824 type="button"
5266 render={4825 onClick={showMoreSettled}
5267 <button4826 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"
5268 type="button"4827 >
5269 onClick={showMoreSettled}4828 <PlusIcon aria-hidden className="size-4 shrink-0" />
5270 className={cn(4829 Show {Math.min(hiddenSettledCount, SETTLED_TAIL_PAGE_COUNT)} more
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",4830 </button>
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>
5286 </li>4831 </li>
5287 ) : null}4832 ) : null}
5288 </ul>4833 </ul>
...@@ -5297,29 +4842,20 @@ export default function Sidebar() {...@@ -5297,29 +4842,20 @@ export default function Sidebar() {
5297 snoozedThreads.length +4842 snoozedThreads.length +
5298 settledThreads.length ===4843 settledThreads.length ===
5299 0 ? (4844 0 ? (
5300 <div4845 <div className="flex flex-col items-center gap-2 px-2 py-6 text-center text-xs text-muted-foreground/60">
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 >
5306 {projects.length === 0 ? (4846 {projects.length === 0 ? (
5307 <>4847 <>
5308 <span className={compact ? "sr-only" : undefined}>No projects yet</span>4848 <span>No projects yet</span>
5309 <button4849 <button
5310 type="button"4850 type="button"
5311 onClick={openAddProjectCommandPalette}4851 onClick={openAddProjectCommandPalette}
5312 aria-label="Add project"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"
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 )}
5317 >4853 >
5318 <PlusIcon className="-mx-0.5 size-3" />4854 <PlusIcon className="-mx-0.5 size-3" />
5319 <span className={compact ? "sr-only" : undefined}>Add project</span>4855 Add project
5320 </button>4856 </button>
5321 </>4857 </>
5322 ) : compact ? null : scopedProjectGroup ? (4858 ) : scopedProjectGroup ? (
5323 `No threads in ${scopedProjectGroup.displayName} yet`4859 `No threads in ${scopedProjectGroup.displayName} yet`
5324 ) : (4860 ) : (
5325 "No threads yet"4861 "No threads yet"
apps/web/src/components/ThreadStatusIndicators.tsx+4-12
...@@ -144,17 +144,14 @@ export function ThreadPullRequestBadgeControl({...@@ -144,17 +144,14 @@ export function ThreadPullRequestBadgeControl({
144 number,144 number,
145 url,145 url,
146 status,146 status,
147 iconOnly = false,
148 onOpenStack,147 onOpenStack,
149 onOpenPullRequest,148 onOpenPullRequest,
150}: {149}: {
151 variant: "underline" | "ghost" | "badge";150 variant: "underline" | "ghost";
152 badge: ThreadPullRequestBadge | null;151 badge: ThreadPullRequestBadge | null;
153 number?: number | undefined;152 number?: number | undefined;
154 url?: string | undefined;153 url?: string | undefined;
155 status: PrStatusIndicator | null;154 status: PrStatusIndicator | null;
156 /** Dense rows drop the number/layer count and keep only the state glyph. */
157 iconOnly?: boolean;
158 onOpenStack: () => void;155 onOpenStack: () => void;
159 onOpenPullRequest: (event: MouseEvent<HTMLAnchorElement>) => void;156 onOpenPullRequest: (event: MouseEvent<HTMLAnchorElement>) => void;
160}) {157}) {
...@@ -171,9 +168,7 @@ export function ThreadPullRequestBadgeControl({...@@ -171,9 +168,7 @@ export function ThreadPullRequestBadgeControl({
171 const className = cn(168 const className = cn(
172 variant === "ghost"169 variant === "ghost"
173 ? buttonVariants({ variant: "ghost", size: "xs" })170 ? buttonVariants({ variant: "ghost", size: "xs" })
174 : variant === "badge"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",
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",
177 "text-xs tabular-nums",172 "text-xs tabular-nums",
178 variant === "ghost" &&173 variant === "ghost" &&
179 "font-normal text-xs! active:scale-100 [--control-icon-color:currentColor]",174 "font-normal text-xs! active:scale-100 [--control-icon-color:currentColor]",
...@@ -183,11 +178,8 @@ export function ThreadPullRequestBadgeControl({...@@ -183,11 +178,8 @@ export function ThreadPullRequestBadgeControl({
183 );178 );
184 const content = (179 const content = (
185 <>180 <>
186 <ThreadPullRequestBadgeIcon181 <ThreadPullRequestBadgeIcon icon={badge?.kind ?? "pull-request"} />
187 icon={badge?.kind ?? "pull-request"}182 {isStack ? badge.layers : linkedCount !== null ? `+${linkedCount}` : number}
188 className={variant === "badge" ? "size-2.5" : undefined}
189 />
190 {iconOnly ? null : isStack ? badge.layers : linkedCount !== null ? `+${linkedCount}` : number}
191 </>183 </>
192 );184 );
193 return (185 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 {...@@ -170,7 +170,6 @@ import {
170import { searchableSetting } from "./settingsSearch";170import { searchableSetting } from "./settingsSearch";
171import { ProjectFavicon } from "../ProjectFavicon";171import { ProjectFavicon } from "../ProjectFavicon";
172import { PanelAnimationsPreview } from "./PanelAnimationsPreview";172import { PanelAnimationsPreview } from "./PanelAnimationsPreview";
173import { CompactSidebarPreview } from "./CompactSidebarPreview";
174173
175const ENVIRONMENT_IDENTIFICATION_LABELS: Record<EnvironmentIdentificationMode, string> = {174const ENVIRONMENT_IDENTIFICATION_LABELS: Record<EnvironmentIdentificationMode, string> = {
176 artwork: "Artwork",175 artwork: "Artwork",
...@@ -524,10 +523,6 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -524,10 +523,6 @@ export function useSettingsRestore(onRestored?: () => void) {
524 ...(theme !== "system" ? ["Theme"] : []),523 ...(theme !== "system" ? ["Theme"] : []),
525 ...(!followSystem ? ["Follow system"] : []),524 ...(!followSystem ? ["Follow system"] : []),
526 ...(themeHalves !== null ? ["Theme mix"] : []),525 ...(themeHalves !== null ? ["Theme mix"] : []),
527 ...(settings.compactSidebarEnabled !== DEFAULT_UNIFIED_SETTINGS.compactSidebarEnabled ||
528 settings.sidebarCompactThreadRows !== DEFAULT_UNIFIED_SETTINGS.sidebarCompactThreadRows
529 ? ["Compact sidebar"]
530 : []),
531 ...(settings.appearanceContrast !== DEFAULT_UNIFIED_SETTINGS.appearanceContrast526 ...(settings.appearanceContrast !== DEFAULT_UNIFIED_SETTINGS.appearanceContrast
532 ? ["Contrast"]527 ? ["Contrast"]
533 : []),528 : []),
...@@ -634,7 +629,6 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -634,7 +629,6 @@ export function useSettingsRestore(onRestored?: () => void) {
634 settings.browserLinkTarget,629 settings.browserLinkTarget,
635 settings.browserAutoShowFloatingPreview,630 settings.browserAutoShowFloatingPreview,
636 settings.appearanceContrast,631 settings.appearanceContrast,
637 settings.compactSidebarEnabled,
638 settings.diffColorScheme,632 settings.diffColorScheme,
639 settings.enableAgentBrowserAccess,633 settings.enableAgentBrowserAccess,
640 settings.confirmQuit,634 settings.confirmQuit,
...@@ -666,7 +660,6 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -666,7 +660,6 @@ export function useSettingsRestore(onRestored?: () => void) {
666 settings.continueThreadsAfterServerUpdate,660 settings.continueThreadsAfterServerUpdate,
667 settings.sidebarAutoSettleAfterDays,661 settings.sidebarAutoSettleAfterDays,
668 settings.sidebarAutoSettleOnMerge,662 settings.sidebarAutoSettleOnMerge,
669 settings.sidebarCompactThreadRows,
670 settings.sidebarProjectGroupingMode,663 settings.sidebarProjectGroupingMode,
671 settings.sidebarThreadPreviewCount,664 settings.sidebarThreadPreviewCount,
672 settings.showSkillsInSlashMenu,665 settings.showSkillsInSlashMenu,
...@@ -744,7 +737,6 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -744,7 +737,6 @@ export function useSettingsRestore(onRestored?: () => void) {
744 }737 }
745 updateSettings({738 updateSettings({
746 appearanceContrast: DEFAULT_UNIFIED_SETTINGS.appearanceContrast,739 appearanceContrast: DEFAULT_UNIFIED_SETTINGS.appearanceContrast,
747 compactSidebarEnabled: DEFAULT_UNIFIED_SETTINGS.compactSidebarEnabled,
748 diffColorScheme: DEFAULT_UNIFIED_SETTINGS.diffColorScheme,740 diffColorScheme: DEFAULT_UNIFIED_SETTINGS.diffColorScheme,
749 timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat,741 timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat,
750 notificationMode: DEFAULT_UNIFIED_SETTINGS.notificationMode,742 notificationMode: DEFAULT_UNIFIED_SETTINGS.notificationMode,
...@@ -762,7 +754,6 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -762,7 +754,6 @@ export function useSettingsRestore(onRestored?: () => void) {
762 panelAnimationDurationMs: DEFAULT_UNIFIED_SETTINGS.panelAnimationDurationMs,754 panelAnimationDurationMs: DEFAULT_UNIFIED_SETTINGS.panelAnimationDurationMs,
763 sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount,755 sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount,
764 sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode,756 sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode,
765 sidebarCompactThreadRows: DEFAULT_UNIFIED_SETTINGS.sidebarCompactThreadRows,
766 sidebarAutoSettleAfterDays: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays,757 sidebarAutoSettleAfterDays: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays,
767 sidebarAutoSettleOnMerge: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleOnMerge,758 sidebarAutoSettleOnMerge: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleOnMerge,
768 responseStreamingMode: DEFAULT_UNIFIED_SETTINGS.responseStreamingMode,759 responseStreamingMode: DEFAULT_UNIFIED_SETTINGS.responseStreamingMode,
...@@ -1143,19 +1134,6 @@ export function AppearanceSettingsPanel() {...@@ -1143,19 +1134,6 @@ export function AppearanceSettingsPanel() {
1143 const [isImportThemeOpen, setIsImportThemeOpen] = useState(false);1134 const [isImportThemeOpen, setIsImportThemeOpen] = useState(false);
1144 const settings = useScopedSettings();1135 const settings = useScopedSettings();
1145 const updateSettings = useUpdateScopedSettings();1136 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 };
1159 const environmentStageLabel = useEnvironmentStageLabel();1137 const environmentStageLabel = useEnvironmentStageLabel();
1160 const showEnvironmentIdentification =1138 const showEnvironmentIdentification =
1161 resolveEnvironmentIdentificationPillLabel(environmentStageLabel) !== null;1139 resolveEnvironmentIdentificationPillLabel(environmentStageLabel) !== null;
...@@ -1442,64 +1420,6 @@ export function AppearanceSettingsPanel() {...@@ -1442,64 +1420,6 @@ export function AppearanceSettingsPanel() {
1442 />1420 />
1443 </SettingsSection>1421 </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
1503 <TypographySection />1423 <TypographySection />
1504 </SettingsPageContainer>1424 </SettingsPageContainer>
1505 );1425 );
apps/web/src/components/settings/SettingsSidebarNav.tsx+7-26
...@@ -24,7 +24,6 @@ import {...@@ -24,7 +24,6 @@ import {
24 XIcon,24 XIcon,
25} from "lucide-react";25} from "lucide-react";
26import { useLocation, useNavigate } from "@tanstack/react-router";26import { useLocation, useNavigate } from "@tanstack/react-router";
27import { useCompactSidebarEnabled } from "../../hooks/useSettings";
2827
29import { Button } from "../ui/button";28import { Button } from "../ui/button";
30import { Input } from "../ui/input";29import { Input } from "../ui/input";
...@@ -112,13 +111,12 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -112,13 +111,12 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
112 (item) => item.to !== "/settings/projects" || isSettingsOverviewVisible(scopeSearch),111 (item) => item.to !== "/settings/projects" || isSettingsOverviewVisible(scopeSearch),
113 );112 );
114 const { isMobile, setOpenMobile, open, setOpen } = useSidebar();113 const { isMobile, setOpenMobile, open, setOpen } = useSidebar();
115 const compactSidebarEnabled = useCompactSidebarEnabled();
116 const searchInputRef = useRef<HTMLInputElement>(null);114 const searchInputRef = useRef<HTMLInputElement>(null);
117 const [query, setQuery] = useState("");115 const [query, setQuery] = useState("");
118 const [activeResultIndex, setActiveResultIndex] = useState(0);116 const [activeResultIndex, setActiveResultIndex] = useState(0);
119 const searchableItems = useAvailableSettingsSearchItems();117 const searchableItems = useAvailableSettingsSearchItems();
120 const results = useMemo(() => searchSettings(query, searchableItems), [query, searchableItems]);118 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;
122 const hasResults = results.length > 0;120 const hasResults = results.length > 0;
123121
124 useEffect(() => {122 useEffect(() => {
...@@ -235,18 +233,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -235,18 +233,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
235 <>233 <>
236 <SidebarContent className="overflow-x-hidden">234 <SidebarContent className="overflow-x-hidden">
237 <SidebarGroup className="gap-2 p-[var(--sidebar-content-inset)]">235 <SidebarGroup className="gap-2 p-[var(--sidebar-content-inset)]">
238 <SidebarMenuButton236 <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">
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">
250 <SearchIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" />237 <SearchIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" />
251 <Input238 <Input
252 ref={searchInputRef}239 ref={searchInputRef}
...@@ -343,14 +330,10 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -343,14 +330,10 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
343 <SidebarMenuItem key={item.to}>330 <SidebarMenuItem key={item.to}>
344 <SidebarMenuButton331 <SidebarMenuButton
345 isActive={isActive}332 isActive={isActive}
346 aria-label={item.label}
347 tooltip={item.label}
348 onClick={() => handleSectionClick(item.to)}333 onClick={() => handleSectionClick(item.to)}
349 >334 >
350 <Icon />335 <Icon />
351 <span className="truncate group-data-[collapsible=icon]:hidden">336 <span className="truncate">{item.label}</span>
352 {item.label}
353 </span>
354 </SidebarMenuButton>337 </SidebarMenuButton>
355 </SidebarMenuItem>338 </SidebarMenuItem>
356 );339 );
...@@ -360,12 +343,10 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -360,12 +343,10 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
360 </SidebarGroup>343 </SidebarGroup>
361 </SidebarContent>344 </SidebarContent>
362 <SidebarFooter className="px-[var(--sidebar-content-inset)] py-1">345 <SidebarFooter className="px-[var(--sidebar-content-inset)] py-1">
363 <div className="contents group-data-[collapsible=icon]:hidden">346 <Suspense fallback={null}>
364 <Suspense fallback={null}>347 <T3ConnectSidebarSignIn />
365 <T3ConnectSidebarSignIn />348 </Suspense>
366 </Suspense>349 <div className="flex items-center gap-1">
367 </div>
368 <div className="flex items-center gap-1 group-data-[collapsible=icon]:flex-col">
369 <div className="min-w-0 flex-1">350 <div className="min-w-0 flex-1">
370 <SidebarUtilityMenu />351 <SidebarUtilityMenu />
371 </div>352 </div>
apps/web/src/components/settings/settingsSearch.ts-8
...@@ -159,14 +159,6 @@ export const SETTINGS_SEARCH_ITEMS = [...@@ -159,14 +159,6 @@ export const SETTINGS_SEARCH_ITEMS = [
159 title: "Panel animations",159 title: "Panel animations",
160 to: "/settings/appearance",160 to: "/settings/appearance",
161 },161 },
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 },
170 {162 {
171 id: "environment-identification",163 id: "environment-identification",
172 title: "Environment identification",164 title: "Environment identification",
apps/web/src/components/sidebar/SidebarChrome.tsx+6-8
...@@ -86,7 +86,7 @@ function SidebarBrand({ onBackdrop }: { onBackdrop: boolean }) {...@@ -86,7 +86,7 @@ function SidebarBrand({ onBackdrop }: { onBackdrop: boolean }) {
86 <Link86 <Link
87 aria-label="Go to threads"87 aria-label="Go to threads"
88 className={cn(88 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",
90 onBackdrop ? "text-white" : "text-foreground",90 onBackdrop ? "text-white" : "text-foreground",
91 )}91 )}
92 to="/"92 to="/"
...@@ -187,12 +187,12 @@ export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() {...@@ -187,12 +187,12 @@ export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() {
187 }, [canGoBack, closeMobileSidebar, navigate]);187 }, [canGoBack, closeMobileSidebar, navigate]);
188188
189 return (189 return (
190 <SidebarMenu className="flex-row items-center group-data-[collapsible=icon]:flex-col">190 <SidebarMenu className="flex-row items-center">
191 {currentFooterPage ? (191 {currentFooterPage ? (
192 <SidebarMenuItem className="min-w-0 flex-1">192 <SidebarMenuItem className="min-w-0 flex-1">
193 <SidebarMenuButton onClick={handleBackClick} aria-label="Back" tooltip="Back">193 <SidebarMenuButton onClick={handleBackClick}>
194 <ArrowLeftIcon />194 <ArrowLeftIcon />
195 <span className="group-data-[collapsible=icon]:hidden">Back</span>195 <span>Back</span>
196 </SidebarMenuButton>196 </SidebarMenuButton>
197 </SidebarMenuItem>197 </SidebarMenuItem>
198 ) : (198 ) : (
...@@ -224,10 +224,8 @@ export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() {...@@ -224,10 +224,8 @@ export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() {
224export const SidebarChromeFooter = memo(function SidebarChromeFooter() {224export const SidebarChromeFooter = memo(function SidebarChromeFooter() {
225 return (225 return (
226 <SidebarFooter className="px-[var(--sidebar-content-inset)] py-1">226 <SidebarFooter className="px-[var(--sidebar-content-inset)] py-1">
227 <div className="contents group-data-[collapsible=icon]:hidden">227 <SidebarProviderUpdatePill />
228 <SidebarProviderUpdatePill />228 <SidebarUpdateArchitectureWarning />
229 <SidebarUpdateArchitectureWarning />
230 </div>
231 <SidebarUtilityMenu />229 <SidebarUtilityMenu />
232 </SidebarFooter>230 </SidebarFooter>
233 );231 );
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 {...@@ -20,10 +20,9 @@ import {
20} from "react";20} from "react";
2121
22import { cn } from "~/lib/utils";22import { cn } from "~/lib/utils";
23import { useCompactSidebarEnabled } from "../../hooks/useSettings";
24import { Button } from "../ui/button";23import { Button } from "../ui/button";
25import { Input } from "../ui/input";24import { Input } from "../ui/input";
26import { SidebarMenuButton, useSidebar } from "../ui/sidebar";25import { SidebarMenuButton } from "../ui/sidebar";
27import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";26import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
2827
29export interface SidebarThreadHeaderProps {28export interface SidebarThreadHeaderProps {
...@@ -70,9 +69,6 @@ export function SidebarThreadHeader({...@@ -70,9 +69,6 @@ export function SidebarThreadHeader({
70 activeSearchResultIndex,69 activeSearchResultIndex,
71 onClearSearch,70 onClearSearch,
72}: SidebarThreadHeaderProps) {71}: SidebarThreadHeaderProps) {
73 const compactEnabled = useCompactSidebarEnabled();
74 const { state, isMobile, setOpen } = useSidebar();
75 const compact = compactEnabled && state === "collapsed" && !isMobile;
76 const resultsVisible = isSearching && searchResultCount > 0;72 const resultsVisible = isSearching && searchResultCount > 0;
77 // Results shrink as the query narrows, so the active index can outrun the73 // Results shrink as the query narrows, so the active index can outrun the
78 // list; pointing aria-activedescendant at a removed option strands the74 // list; pointing aria-activedescendant at a removed option strands the
...@@ -83,24 +79,10 @@ export function SidebarThreadHeader({...@@ -83,24 +79,10 @@ export function SidebarThreadHeader({
83 : "New thread";79 : "New thread";
8480
85 return (81 return (
86 <div className={cn("flex items-center gap-1", compact && "flex-col")}>82 <div className="flex items-center gap-1">
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}
98 <div83 <div
99 ref={searchFieldRef}84 ref={searchFieldRef}
100 className={cn(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"
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 )}
104 >86 >
105 <SearchIcon className="size-4 shrink-0 text-[var(--sidebar-icon-color)]" />87 <SearchIcon className="size-4 shrink-0 text-[var(--sidebar-icon-color)]" />
106 <Input88 <Input
...@@ -142,12 +124,7 @@ export function SidebarThreadHeader({...@@ -142,12 +124,7 @@ export function SidebarThreadHeader({
142 </div>124 </div>
143 {/* Segmented well: the icons read as one control instead of three loose125 {/* Segmented well: the icons read as one control instead of three loose
144 buttons competing with the search field beside them. */}126 buttons competing with the search field beside them. */}
145 <div127 <div className="flex shrink-0 items-center rounded-md bg-sidebar-control-surface/60 p-px">
146 className={cn(
147 "flex shrink-0 items-center rounded-md bg-sidebar-control-surface/60 p-px",
148 compact && "flex-col",
149 )}
150 >
151 {hasProjects ? (128 {hasProjects ? (
152 <>129 <>
153 {projectScope}130 {projectScope}
apps/web/src/components/sidebar/SidebarUpdatePill.tsx+1-1
...@@ -348,7 +348,7 @@ function SidebarUpdateControl() {...@@ -348,7 +348,7 @@ function SidebarUpdateControl() {
348 );348 );
349349
350 return (350 return (
351 <SidebarMenuItem className="ml-auto shrink-0 group-data-[collapsible=icon]:hidden">351 <SidebarMenuItem className="ml-auto shrink-0">
352 <Popover352 <Popover
353 handle={releaseNotesPopoverHandle}353 handle={releaseNotesPopoverHandle}
354 onOpenChange={(open, details) => {354 onOpenChange={(open, details) => {
apps/web/src/components/ui/sidebar.tsx-3
...@@ -591,11 +591,9 @@ function SidebarSeparator({ className, ...props }: React.ComponentProps<typeof S...@@ -591,11 +591,9 @@ function SidebarSeparator({ className, ...props }: React.ComponentProps<typeof S
591function SidebarContent({591function SidebarContent({
592 className,592 className,
593 fixedHeader,593 fixedHeader,
594 fixedFooter,
595 ...props594 ...props
596}: React.ComponentProps<"div"> & {595}: React.ComponentProps<"div"> & {
597 fixedHeader?: React.ReactNode;596 fixedHeader?: React.ReactNode;
598 fixedFooter?: React.ReactNode;
599}) {597}) {
600 return (598 return (
601 <>599 <>
...@@ -619,7 +617,6 @@ function SidebarContent({...@@ -619,7 +617,6 @@ function SidebarContent({
619 {...props}617 {...props}
620 />618 />
621 </ScrollArea>619 </ScrollArea>
622 {fixedFooter ? <div className="w-full shrink-0">{fixedFooter}</div> : null}
623 </>620 </>
624 );621 );
625}622}
apps/web/src/hooks/useSettings.ts-7
...@@ -379,13 +379,6 @@ export function useLegacySidebarEnabled(): boolean {...@@ -379,13 +379,6 @@ export function useLegacySidebarEnabled(): boolean {
379 return settingsHydrated && legacySidebarEnabled;379 return settingsHydrated && legacySidebarEnabled;
380}380}
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
389/** Read current settings for one environment, merged with client-local preferences. */382/** Read current settings for one environment, merged with client-local preferences. */
390export function useEnvironmentSettings<T = UnifiedSettings>(383export function useEnvironmentSettings<T = UnifiedSettings>(
391 environmentId: EnvironmentId,384 environmentId: EnvironmentId,
apps/web/src/timestampFormat.test.ts-12
...@@ -228,15 +228,3 @@ describe("formatElapsedDurationLabel", () => {...@@ -228,15 +228,3 @@ describe("formatElapsedDurationLabel", () => {
228 expect(formatElapsedDurationLabel("2026-04-03T12:00:00.000Z")).toBe("4d");228 expect(formatElapsedDurationLabel("2026-04-03T12:00:00.000Z")).toBe("4d");
229 });229 });
230});230});
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 =...@@ -196,10 +196,10 @@ export type RelativeTimeState =
196 | { status: "invalid" }196 | { status: "invalid" }
197 | { status: "relative"; value: string; suffix: string | null };197 | { 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 {
200 const date = parseTimestampDate(isoDate);200 const date = parseTimestampDate(isoDate);
201 if (!date) return null;201 if (!date) return null;
202 const diffMs = nowMs - date.getTime();202 const diffMs = Date.now() - date.getTime();
203 if (diffMs < 0) return { value: "just now", suffix: null };203 if (diffMs < 0) return { value: "just now", suffix: null };
204 const seconds = Math.floor(diffMs / 1000);204 const seconds = Math.floor(diffMs / 1000);
205 if (seconds < 60) return { value: "just now", suffix: null };205 if (seconds < 60) return { value: "just now", suffix: null };
...@@ -211,8 +211,8 @@ export function formatRelativeTime(isoDate: string, nowMs = Date.now()): Relativ...@@ -211,8 +211,8 @@ export function formatRelativeTime(isoDate: string, nowMs = Date.now()): Relativ
211 return { value: `${days}d`, suffix: "ago" };211 return { value: `${days}d`, suffix: "ago" };
212}212}
213213
214export function formatRelativeTimeLabel(isoDate: string, nowMs = Date.now()) {214export function formatRelativeTimeLabel(isoDate: string) {
215 const relative = formatRelativeTime(isoDate, nowMs);215 const relative = formatRelativeTime(isoDate);
216 if (!relative) return "";216 if (!relative) return "";
217 return relative.suffix ? `${relative.value} ${relative.suffix}` : relative.value;217 return relative.suffix ? `${relative.value} ${relative.suffix}` : relative.value;
218}218}
apps/web/src/workspaceTitlebar.ts+1-1
...@@ -1,2 +1,2 @@...@@ -1,2 +1,2 @@
1export const COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS =1export 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", () => {...@@ -482,18 +482,7 @@ describe("ClientSettings environment identification", () => {
482482
483describe("ClientSettings sidebar", () => {483describe("ClientSettings sidebar", () => {
484 it("defaults to the current sidebar", () => {484 it("defaults to the current sidebar", () => {
485 const settings = decodeClientSettings({});485 expect(decodeClientSettings({}).legacySidebarEnabled).toBe(false);
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);
497 });486 });
498487
499 it("drops the retired sidebar v2 beta keys, resetting everyone to the default", () => {488 it("drops the retired sidebar v2 beta keys, resetting everyone to the default", () => {
...@@ -506,6 +495,14 @@ describe("ClientSettings sidebar", () => {...@@ -506,6 +495,14 @@ describe("ClientSettings sidebar", () => {
506 expect(decoded).not.toHaveProperty("sidebarV2ConfiguredByUser");495 expect(decoded).not.toHaveProperty("sidebarV2ConfiguredByUser");
507 });496 });
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
509 it("preserves an explicit legacy sidebar opt-in", () => {506 it("preserves an explicit legacy sidebar opt-in", () => {
510 expect(decodeClientSettings({ legacySidebarEnabled: true }).legacySidebarEnabled).toBe(true);507 expect(decodeClientSettings({ legacySidebarEnabled: true }).legacySidebarEnabled).toBe(true);
511 expect(decodeClientSettingsPatch({ legacySidebarEnabled: true }).legacySidebarEnabled).toBe(508 expect(decodeClientSettingsPatch({ legacySidebarEnabled: true }).legacySidebarEnabled).toBe(
packages/contracts/src/settings.ts-4
...@@ -437,7 +437,6 @@ export const ClientSettingsSchema = Schema.Struct({...@@ -437,7 +437,6 @@ export const ClientSettingsSchema = Schema.Struct({
437 // old keys, so everyone, including prior beta opt-outs, resets to the new437 // old keys, so everyone, including prior beta opt-outs, resets to the new
438 // default sidebar.438 // default sidebar.
439 legacySidebarEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),439 legacySidebarEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
440 compactSidebarEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
441 sidebarProjectGroupingMode: SidebarProjectGroupingMode.pipe(440 sidebarProjectGroupingMode: SidebarProjectGroupingMode.pipe(
442 Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE)),441 Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE)),
443 ),442 ),
...@@ -451,7 +450,6 @@ export const ClientSettingsSchema = Schema.Struct({...@@ -451,7 +450,6 @@ export const ClientSettingsSchema = Schema.Struct({
451 sidebarThreadSortOrder: SidebarThreadSortOrder.pipe(450 sidebarThreadSortOrder: SidebarThreadSortOrder.pipe(
452 Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_SORT_ORDER)),451 Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_SORT_ORDER)),
453 ),452 ),
454 sidebarCompactThreadRows: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
455 sidebarThreadPreviewCount: SidebarThreadPreviewCount.pipe(453 sidebarThreadPreviewCount: SidebarThreadPreviewCount.pipe(
456 Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_PREVIEW_COUNT)),454 Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_PREVIEW_COUNT)),
457 ),455 ),
...@@ -1504,14 +1502,12 @@ export const ClientSettingsPatch = Schema.Struct({...@@ -1504,14 +1502,12 @@ export const ClientSettingsPatch = Schema.Struct({
1504 proactivePanelsEnabled: Schema.optionalKey(Schema.Boolean),1502 proactivePanelsEnabled: Schema.optionalKey(Schema.Boolean),
1505 showSkillsInSlashMenu: Schema.optionalKey(Schema.Boolean),1503 showSkillsInSlashMenu: Schema.optionalKey(Schema.Boolean),
1506 legacySidebarEnabled: Schema.optionalKey(Schema.Boolean),1504 legacySidebarEnabled: Schema.optionalKey(Schema.Boolean),
1507 compactSidebarEnabled: Schema.optionalKey(Schema.Boolean),
1508 sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode),1505 sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode),
1509 sidebarProjectGroupingOverrides: Schema.optionalKey(1506 sidebarProjectGroupingOverrides: Schema.optionalKey(
1510 Schema.Record(TrimmedNonEmptyString, SidebarProjectGroupingMode),1507 Schema.Record(TrimmedNonEmptyString, SidebarProjectGroupingMode),
1511 ),1508 ),
1512 sidebarProjectSortOrder: Schema.optionalKey(SidebarProjectSortOrder),1509 sidebarProjectSortOrder: Schema.optionalKey(SidebarProjectSortOrder),
1513 sidebarThreadSortOrder: Schema.optionalKey(SidebarThreadSortOrder),1510 sidebarThreadSortOrder: Schema.optionalKey(SidebarThreadSortOrder),
1514 sidebarCompactThreadRows: Schema.optionalKey(Schema.Boolean),
1515 sidebarThreadPreviewCount: Schema.optionalKey(SidebarThreadPreviewCount),1511 sidebarThreadPreviewCount: Schema.optionalKey(SidebarThreadPreviewCount),
1516 timestampFormat: Schema.optionalKey(TimestampFormat),1512 timestampFormat: Schema.optionalKey(TimestampFormat),
1517 snapShotEnabled: Schema.optionalKey(Schema.Boolean),1513 snapShotEnabled: Schema.optionalKey(Schema.Boolean),