| ... | @@ -46,12 +46,10 @@ import { | ... | @@ -46,12 +46,10 @@ import { |
| 46 | CircleDashedIcon, | 46 | CircleDashedIcon, |
| 47 | ClockIcon, | 47 | ClockIcon, |
| 48 | FolderIcon, | 48 | FolderIcon, |
| 49 | FolderPlusIcon, | | |
| 50 | GitBranchIcon, | 49 | GitBranchIcon, |
| 51 | PinIcon, | 50 | PinIcon, |
| 52 | PinOffIcon, | 51 | PinOffIcon, |
| 53 | PlusIcon, | 52 | PlusIcon, |
| 54 | SearchIcon, | | |
| 55 | SettingsIcon, | 53 | SettingsIcon, |
| 56 | SquarePenIcon, | 54 | SquarePenIcon, |
| 57 | TerminalIcon, | 55 | TerminalIcon, |
| ... | @@ -218,7 +216,6 @@ import { | ... | @@ -218,7 +216,6 @@ import { |
| 218 | import { useThreadRunningTerminalIds } from "../state/terminalSessions"; | 216 | import { useThreadRunningTerminalIds } from "../state/terminalSessions"; |
| 219 | import { stackedThreadToast, toastManager } from "./ui/toast"; | 217 | import { stackedThreadToast, toastManager } from "./ui/toast"; |
| 220 | import { Button } from "./ui/button"; | 218 | import { Button } from "./ui/button"; |
| 221 | import { Input } from "./ui/input"; | | |
| 222 | import { | 219 | import { |
| 223 | Combobox, | 220 | Combobox, |
| 224 | ComboboxEmpty, | 221 | ComboboxEmpty, |
| ... | @@ -229,8 +226,9 @@ import { | ... | @@ -229,8 +226,9 @@ import { |
| 229 | ComboboxTrigger, | 226 | ComboboxTrigger, |
| 230 | useComboboxFilter, | 227 | useComboboxFilter, |
| 231 | } from "./ui/combobox"; | 228 | } from "./ui/combobox"; |
| 232 | import { SidebarContent, SidebarGroup, SidebarMenuButton, useSidebar } from "./ui/sidebar"; | 229 | import { SidebarContent, SidebarGroup, useSidebar } from "./ui/sidebar"; |
| 233 | import { SidebarChromeFooter, SidebarChromeHeader } from "./sidebar/SidebarChrome"; | 230 | import { SidebarChromeFooter, SidebarChromeHeader } from "./sidebar/SidebarChrome"; |
| | 231 | import { SidebarHeaderIconButton, SidebarThreadHeader } from "./sidebar/SidebarThreadHeader"; |
| 234 | import { Popover, PopoverPopup, PopoverTrigger } from "./ui/popover"; | 232 | import { Popover, PopoverPopup, PopoverTrigger } from "./ui/popover"; |
| 235 | import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "./ui/tooltip"; | 233 | import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "./ui/tooltip"; |
| 236 | import { | 234 | import { |
| ... | @@ -2370,21 +2368,19 @@ export default function Sidebar() { | ... | @@ -2370,21 +2368,19 @@ export default function Sidebar() { |
| 2370 | const projectScopeFilter = useComboboxFilter(); | 2368 | const projectScopeFilter = useComboboxFilter(); |
| 2371 | // Filtering derives from the same React state that controls the input, so | 2369 | // Filtering derives from the same React state that controls the input, so |
| 2372 | // the visible query and the visible list can never desync — the peer wiring | 2370 | // the visible query and the visible list can never desync — the peer wiring |
| 2373 | // in DiffPanel and BranchToolbarBranchSelector. "All projects" is a scope | 2371 | // in DiffPanel and BranchToolbarBranchSelector. "All projects" is the default |
| 2374 | // reset, not a searchable entry: it only shows while a project scope is | 2372 | // row, not a searchable entry: it heads the list while the query is empty and |
| 2375 | // active (there is something to reset) and the query is empty, so it can't | 2373 | // drops out while filtering, so it can't outrank a project match under |
| 2376 | // outrank a project match under autoHighlight and no-hit queries reach the | 2374 | // autoHighlight and no-hit queries reach the empty state. |
| 2377 | // empty state. | | |
| 2378 | const filteredProjectScopeItems = useMemo( | 2375 | const filteredProjectScopeItems = useMemo( |
| 2379 | () => | 2376 | () => |
| 2380 | filterSidebarProjectScopeItems({ | 2377 | filterSidebarProjectScopeItems({ |
| 2381 | items: projectScopeItems, | 2378 | items: projectScopeItems, |
| 2382 | activeScopeKey: projectScopeKey, | | |
| 2383 | query: projectScopeMenuState.query, | 2379 | query: projectScopeMenuState.query, |
| 2384 | matches: (item, query) => | 2380 | matches: (item, query) => |
| 2385 | projectScopeFilter.contains(item, query, (candidate) => candidate.label), | 2381 | projectScopeFilter.contains(item, query, (candidate) => candidate.label), |
| 2386 | }), | 2382 | }), |
| 2387 | [projectScopeFilter, projectScopeItems, projectScopeKey, projectScopeMenuState.query], | 2383 | [projectScopeFilter, projectScopeItems, projectScopeMenuState.query], |
| 2388 | ); | 2384 | ); |
| 2389 | const scopedProjectGroup = useMemo( | 2385 | const scopedProjectGroup = useMemo( |
| 2390 | () => | 2386 | () => |
| ... | @@ -2457,6 +2453,8 @@ export default function Sidebar() { | ... | @@ -2457,6 +2453,8 @@ export default function Sidebar() { |
| 2457 | }, | 2453 | }, |
| 2458 | [isMobile, router, setOpenMobile], | 2454 | [isMobile, router, setOpenMobile], |
| 2459 | ); | 2455 | ); |
| | 2456 | // Anchor for the scope popup: the header search field, not its icon trigger. |
| | 2457 | const headerSearchRef = useRef<HTMLDivElement | null>(null); |
| 2460 | // Safari can send a click after Ctrl+click opens settings. Ignore that one | 2458 | // Safari can send a click after Ctrl+click opens settings. Ignore that one |
| 2461 | // selection, then clear the guard when the picker opens again. | 2459 | // selection, then clear the guard when the picker opens again. |
| 2462 | const suppressNextScopeChangeRef = useRef(false); | 2460 | const suppressNextScopeChangeRef = useRef(false); |
| ... | @@ -4325,100 +4323,11 @@ export default function Sidebar() { | ... | @@ -4325,100 +4323,11 @@ export default function Sidebar() { |
| 4325 | fixedHeader={ | 4323 | fixedHeader={ |
| 4326 | // Lifted above the stage backdrop, whose fade bleeds below the | 4324 | // Lifted above the stage backdrop, whose fade bleeds below the |
| 4327 | // header and would otherwise paint across the search row's outline. | 4325 | // header and would otherwise paint across the search row's outline. |
| 4328 | <SidebarGroup className="relative z-[1] gap-1 p-[var(--sidebar-content-inset)]"> | 4326 | <SidebarGroup className="relative z-[1] p-[var(--sidebar-content-inset)]"> |
| 4329 | <div className="flex items-center gap-1"> | 4327 | <SidebarThreadHeader |
| 4330 | <div 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"> | 4328 | searchFieldRef={headerSearchRef} |
| 4331 | <SearchIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" /> | 4329 | hasProjects={projectGroups.length > 0} |
| 4332 | <Input | 4330 | projectScope={ |
| 4333 | ref={threadSearchInputRef} | | |
| 4334 | nativeInput | | |
| 4335 | unstyled | | |
| 4336 | type="search" | | |
| 4337 | value={threadSearchQuery} | | |
| 4338 | onChange={(event) => { | | |
| 4339 | setThreadSearchQuery(event.currentTarget.value); | | |
| 4340 | setActiveSearchResultIndex(0); | | |
| 4341 | }} | | |
| 4342 | onKeyDown={handleThreadSearchKeyDown} | | |
| 4343 | placeholder="Search threads or PRs" | | |
| 4344 | aria-label="Search threads" | | |
| 4345 | role="combobox" | | |
| 4346 | aria-autocomplete="list" | | |
| 4347 | aria-expanded={isSearchingThreads && threadSearchResults.length > 0} | | |
| 4348 | aria-controls={ | | |
| 4349 | isSearchingThreads && threadSearchResults.length > 0 | | |
| 4350 | ? "sidebar-thread-search-results" | | |
| 4351 | : undefined | | |
| 4352 | } | | |
| 4353 | aria-activedescendant={ | | |
| 4354 | isSearchingThreads && threadSearchResults[activeSearchResultIndex] | | |
| 4355 | ? `sidebar-thread-search-result-${activeSearchResultIndex}` | | |
| 4356 | : undefined | | |
| 4357 | } | | |
| 4358 | className="min-w-0 flex-1 [&_[data-slot=input]]:h-auto [&_[data-slot=input]]:p-0 [&_[data-slot=input]]:leading-normal [&_[data-slot=input]]:text-sm [&_[data-slot=input]]:font-medium [&_[data-slot=input]]:text-sidebar-foreground [&_[data-slot=input]]:placeholder:text-sidebar-muted-foreground" | | |
| 4359 | /> | | |
| 4360 | {isSearchingThreads ? ( | | |
| 4361 | <Button | | |
| 4362 | type="button" | | |
| 4363 | size="icon-micro" | | |
| 4364 | variant="ghost" | | |
| 4365 | className="shrink-0 text-sidebar-muted-foreground hover:bg-sidebar-control-surface hover:text-sidebar-foreground" | | |
| 4366 | aria-label="Clear thread search" | | |
| 4367 | onClick={() => { | | |
| 4368 | clearThreadSearch(); | | |
| 4369 | threadSearchInputRef.current?.focus(); | | |
| 4370 | }} | | |
| 4371 | > | | |
| 4372 | <XIcon className="size-3" /> | | |
| 4373 | </Button> | | |
| 4374 | ) : null} | | |
| 4375 | </div> | | |
| 4376 | <div className="shrink-0"> | | |
| 4377 | <Tooltip> | | |
| 4378 | <TooltipTrigger | | |
| 4379 | render={ | | |
| 4380 | <SidebarMenuButton | | |
| 4381 | size="icon" | | |
| 4382 | type="button" | | |
| 4383 | className="relative focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar" | | |
| 4384 | onClick={handleNewThreadClick} | | |
| 4385 | disabled={projects.length === 0} | | |
| 4386 | aria-label="New thread" | | |
| 4387 | /> | | |
| 4388 | } | | |
| 4389 | > | | |
| 4390 | <SquarePenIcon /> | | |
| 4391 | <span | | |
| 4392 | className="pointer-events-none absolute left-1/2 top-1/2 size-[max(100%,3rem)] -translate-1/2 pointer-fine:hidden" | | |
| 4393 | aria-hidden="true" | | |
| 4394 | /> | | |
| 4395 | </TooltipTrigger> | | |
| 4396 | <TooltipPopup side="right"> | | |
| 4397 | {projectGroups.length > 1 ? ( | | |
| 4398 | <span className="flex flex-col gap-0.5"> | | |
| 4399 | <span> | | |
| 4400 | {newThreadShortcutLabel | | |
| 4401 | ? `New thread (${newThreadShortcutLabel})` | | |
| 4402 | : "New thread"} | | |
| 4403 | </span> | | |
| 4404 | <span className="text-muted-foreground"> | | |
| 4405 | New thread in current project: Shift+click | | |
| 4406 | {newThreadInProjectShortcutLabel | | |
| 4407 | ? ` (${newThreadInProjectShortcutLabel})` | | |
| 4408 | : ""} | | |
| 4409 | </span> | | |
| 4410 | </span> | | |
| 4411 | ) : newThreadShortcutLabel ? ( | | |
| 4412 | `New thread (${newThreadShortcutLabel})` | | |
| 4413 | ) : ( | | |
| 4414 | "New thread" | | |
| 4415 | )} | | |
| 4416 | </TooltipPopup> | | |
| 4417 | </Tooltip> | | |
| 4418 | </div> | | |
| 4419 | </div> | | |
| 4420 | {projectGroups.length > 0 ? ( | | |
| 4421 | <div className="flex items-center gap-1"> | | |
| 4422 | <Combobox | 4331 | <Combobox |
| 4423 | items={projectScopeItems} | 4332 | items={projectScopeItems} |
| 4424 | filteredItems={filteredProjectScopeItems} | 4333 | filteredItems={filteredProjectScopeItems} |
| ... | @@ -4445,34 +4354,33 @@ export default function Sidebar() { | ... | @@ -4445,34 +4354,33 @@ export default function Sidebar() { |
| 4445 | > | 4354 | > |
| 4446 | <ComboboxTrigger | 4355 | <ComboboxTrigger |
| 4447 | render={ | 4356 | render={ |
| 4448 | <SidebarMenuButton | 4357 | <SidebarHeaderIconButton |
| 4449 | aria-label="Filter threads by project" | 4358 | label={ |
| 4450 | className="min-w-0 flex-1 ps-[calc(var(--sidebar-row-content-inset)-1px)] focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar" | 4359 | scopedProjectGroup |
| | 4360 | ? `Filter threads by project: ${scopedProjectGroup.displayName}` |
| | 4361 | : "Filter threads by project" |
| | 4362 | } |
| 4451 | /> | 4363 | /> |
| 4452 | } | 4364 | } |
| 4453 | > | 4365 | > |
| 4454 | {scopedProjectGroup ? ( | 4366 | {scopedProjectGroup ? ( |
| | 4367 | // Wrapped so the button's direct-child svg color rule cannot override |
| | 4368 | // a project's own icon color. |
| 4455 | <span className="flex shrink-0"> | 4369 | <span className="flex shrink-0"> |
| 4456 | <ProjectFavicon project={scopedProjectGroup} className="size-4" /> | 4370 | <ProjectFavicon project={scopedProjectGroup} className="size-4" /> |
| 4457 | </span> | 4371 | </span> |
| 4458 | ) : ( | 4372 | ) : ( |
| 4459 | <FolderIcon className="size-4 shrink-0" /> | 4373 | <FolderIcon className="size-4" /> |
| 4460 | )} | 4374 | )} |
| 4461 | <span className="min-w-0 flex-1 truncate"> | | |
| 4462 | {scopedProjectGroup?.displayName ?? "All projects"} | | |
| 4463 | </span> | | |
| 4464 | {scopedProjectGroup && showProjectEnvironments ? ( | | |
| 4465 | <ProjectEnvironmentBadge | | |
| 4466 | group={scopedProjectGroup} | | |
| 4467 | primaryEnvironmentId={primaryEnvironmentId} | | |
| 4468 | machineByEnvironmentId={environmentMachineById} | | |
| 4469 | /> | | |
| 4470 | ) : null} | | |
| 4471 | <ChevronDownIcon className="-mr-px size-4 shrink-0" /> | | |
| 4472 | </ComboboxTrigger> | 4375 | </ComboboxTrigger> |
| 4473 | <ComboboxPopup | 4376 | <ComboboxPopup |
| 4474 | align="start" | 4377 | align="start" |
| 4475 | className="w-(--anchor-width) min-w-0 overflow-hidden" | 4378 | // Anchored to the search field, not the 28px trigger: the |
| | 4379 | // popup opens under the field, is at least as wide as it, |
| | 4380 | // and grows to fit project names up to a cap, past which |
| | 4381 | // the rows truncate. |
| | 4382 | anchor={headerSearchRef} |
| | 4383 | className="max-w-[min(18rem,var(--available-width))] overflow-hidden" |
| 4476 | > | 4384 | > |
| 4477 | <ComboboxSearchInput | 4385 | <ComboboxSearchInput |
| 4478 | aria-label="Search projects" | 4386 | aria-label="Search projects" |
| ... | @@ -4552,28 +4460,25 @@ export default function Sidebar() { | ... | @@ -4552,28 +4460,25 @@ export default function Sidebar() { |
| 4552 | </ComboboxList> | 4460 | </ComboboxList> |
| 4553 | </ComboboxPopup> | 4461 | </ComboboxPopup> |
| 4554 | </Combobox> | 4462 | </Combobox> |
| 4555 | <Tooltip> | 4463 | } |
| 4556 | <TooltipTrigger | 4464 | onNewProject={openAddProjectCommandPalette} |
| 4557 | render={ | 4465 | onNewThread={handleNewThreadClick} |
| 4558 | <SidebarMenuButton | 4466 | newThreadDisabled={projects.length === 0} |
| 4559 | size="icon" | 4467 | newThreadShortcutLabel={newThreadShortcutLabel} |
| 4560 | className="relative shrink-0 focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar" | 4468 | newThreadInProjectShortcutLabel={newThreadInProjectShortcutLabel} |
| 4561 | onClick={openAddProjectCommandPalette} | 4469 | showNewThreadInProjectHint={projectGroups.length > 1} |
| 4562 | type="button" | 4470 | searchInputRef={threadSearchInputRef} |
| 4563 | aria-label="New project" | 4471 | searchQuery={threadSearchQuery} |
| 4564 | /> | 4472 | onSearchQueryChange={(value) => { |
| 4565 | } | 4473 | setThreadSearchQuery(value); |
| 4566 | > | 4474 | setActiveSearchResultIndex(0); |
| 4567 | <FolderPlusIcon /> | 4475 | }} |
| 4568 | <span | 4476 | onSearchKeyDown={handleThreadSearchKeyDown} |
| 4569 | className="pointer-events-none absolute left-1/2 top-1/2 size-[max(100%,3rem)] -translate-1/2 pointer-fine:hidden" | 4477 | isSearching={isSearchingThreads} |
| 4570 | aria-hidden="true" | 4478 | searchResultCount={threadSearchResults.length} |
| 4571 | /> | 4479 | activeSearchResultIndex={activeSearchResultIndex} |
| 4572 | </TooltipTrigger> | 4480 | onClearSearch={clearThreadSearch} |
| 4573 | <TooltipPopup side="right">New project</TooltipPopup> | 4481 | /> |
| 4574 | </Tooltip> | | |
| 4575 | </div> | | |
| 4576 | ) : null} | | |
| 4577 | </SidebarGroup> | 4482 | </SidebarGroup> |
| 4578 | } | 4483 | } |
| 4579 | > | 4484 | > |