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