From a2a542da45f36afb296b93aa524ed01ee00d7da0 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 5 Sep 2026 21:17:30 -0700 Subject: [PATCH] feat(palette): seed Cmd+J filter from sidebar show scope When opening Cmd+J, the palette's host and project filters now initialize from the sidebar's current Show scope, so results match the user's sidebar view. The palette can still be cleared or changed per open; sidebar never reads back palette filters. --- .../components/cmd-j/palette-filter.test.ts | 63 +++++++++++++++++++ .../src/components/cmd-j/palette-filter.ts | 40 ++++++++++++ ...rktree-jump-palette-selection-lifecycle.ts | 16 ++++- .../use-worktree-jump-palette-store-state.ts | 6 ++ 4 files changed, 123 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/cmd-j/palette-filter.test.ts b/src/renderer/src/components/cmd-j/palette-filter.test.ts index fcb62102f82..913a9c4a3d9 100644 --- a/src/renderer/src/components/cmd-j/palette-filter.test.ts +++ b/src/renderer/src/components/cmd-j/palette-filter.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import type { ExecutionHostId } from '../../../../shared/execution-host' import { addPaletteFilterValues, + buildPaletteFilterFromSidebarScope, buildPaletteFilterPredicate, clearPaletteFilterField, EMPTY_PALETTE_FILTER, @@ -212,3 +213,65 @@ describe('buildPaletteFilterPredicate', () => { expect(withProject?.matchesGroupHostId('ssh:builder')).toBe(false) }) }) + +describe('buildPaletteFilterFromSidebarScope', () => { + const allHosts = { workspaceHostScope: 'all', visibleWorkspaceHostIds: null } as const + + it('opens unfiltered when the sidebar shows every host and project', () => { + expect(buildPaletteFilterFromSidebarScope({ ...allHosts, filterRepoIds: [] }, model)).toBe( + EMPTY_PALETTE_FILTER + ) + }) + + it('seeds the host chips from the sidebar host scope', () => { + expect( + buildPaletteFilterFromSidebarScope( + { workspaceHostScope: 'ssh:builder', visibleWorkspaceHostIds: null, filterRepoIds: [] }, + model + ) + ).toEqual(filterOf(['ssh:builder'], [])) + expect( + buildPaletteFilterFromSidebarScope( + { + workspaceHostScope: 'all', + visibleWorkspaceHostIds: ['runtime:env-1', 'local'], + filterRepoIds: [] + }, + model + ) + ).toEqual(filterOf(['local', 'runtime:env-1'], [])) + }) + + it('maps sidebar repo picks onto the project rows that contain them', () => { + expect( + buildPaletteFilterFromSidebarScope({ ...allHosts, filterRepoIds: ['r2'] }, model) + ).toEqual(filterOf([], ['project:p1'])) + }) + + it('treats a scope that covers every option as no filter', () => { + // Every host explicitly listed is the same as "all hosts": no chips to clear. + expect( + buildPaletteFilterFromSidebarScope( + { + workspaceHostScope: 'all', + visibleWorkspaceHostIds: ['local', 'ssh:builder', 'runtime:env-1'], + filterRepoIds: ['r1', 'r3'] + }, + model + ) + ).toBe(EMPTY_PALETTE_FILTER) + }) + + it('falls back to a global search when the scope matches nothing the palette can show', () => { + expect( + buildPaletteFilterFromSidebarScope( + { + workspaceHostScope: 'ssh:gone', + visibleWorkspaceHostIds: null, + filterRepoIds: ['r-gone'] + }, + model + ) + ).toBe(EMPTY_PALETTE_FILTER) + }) +}) diff --git a/src/renderer/src/components/cmd-j/palette-filter.ts b/src/renderer/src/components/cmd-j/palette-filter.ts index f521a6ad92a..18e583d3d17 100644 --- a/src/renderer/src/components/cmd-j/palette-filter.ts +++ b/src/renderer/src/components/cmd-j/palette-filter.ts @@ -1,4 +1,5 @@ import type { ExecutionHostId } from '../../../../shared/execution-host' +import { getVisibleWorkspaceHostIdSet } from '../sidebar/visible-worktree-host-scope' import type { Worktree } from '../../../../shared/worktree/types' import { resolveRepoFilterHostId, @@ -119,6 +120,45 @@ export function reconcilePaletteFilter( return { hostIds, projectKeys } } +export type SidebarScopeForPaletteFilter = Parameters[0] & { + filterRepoIds: readonly string[] +} + +/** + * Seeds the palette's host/project selection from the sidebar's "Show" scope so + * Cmd+J opens on the same slice the user is looking at. One-way: the user can + * clear or change it per open, and the sidebar never reads it back. A field + * that would select every option is left empty — that is not a filter, just chips. + */ +export function buildPaletteFilterFromSidebarScope( + scope: SidebarScopeForPaletteFilter, + model: PaletteFilterModel +): PaletteFilterState { + const visibleHostIds = getVisibleWorkspaceHostIdSet(scope) + let hostIds: readonly string[] = [] + if (visibleHostIds) { + const available = model.hosts.filter((host) => visibleHostIds.has(host.id as ExecutionHostId)) + hostIds = available.length < model.hosts.length ? available.map((host) => host.id).sort() : [] + } + + let projectKeys: readonly string[] = [] + if (scope.filterRepoIds.length > 0) { + const selectedRepoIds = new Set(scope.filterRepoIds) + const available = model.projects.filter((project) => + (model.repoIdsByProjectKey.get(project.id) ?? []).some((repoId) => + selectedRepoIds.has(repoId) + ) + ) + projectKeys = + available.length < model.projects.length ? available.map((project) => project.id).sort() : [] + } + + if (hostIds.length === 0 && projectKeys.length === 0) { + return EMPTY_PALETTE_FILTER + } + return { hostIds, projectKeys } +} + export type PaletteFilterPredicate = { matchesWorktree: (worktree: Pick) => boolean /** Keyed on the row, not a repo: one project row can span repos on several hosts. */ diff --git a/src/renderer/src/components/use-worktree-jump-palette-selection-lifecycle.ts b/src/renderer/src/components/use-worktree-jump-palette-selection-lifecycle.ts index 8906add43e2..f13a4ce423e 100644 --- a/src/renderer/src/components/use-worktree-jump-palette-selection-lifecycle.ts +++ b/src/renderer/src/components/use-worktree-jump-palette-selection-lifecycle.ts @@ -4,7 +4,7 @@ import { queueBrowserFocusRequest } from '@/components/browser-pane/host-guest/browser-focus' import { captureCmdJActiveGroupSnapshot } from '@/components/cmd-j/quick-action-context' -import { EMPTY_PALETTE_FILTER } from '@/components/cmd-j/palette-filter' +import { buildPaletteFilterFromSidebarScope } from '@/components/cmd-j/palette-filter' import { resolvePaletteFocusRestoreTarget } from '@/components/cmd-j/palette-focus-restore-target' import { CREATE_WORKTREE_ITEM_ID, @@ -12,6 +12,7 @@ import { } from '@/lib/worktree-palette-create-action' import { useAppStore } from '@/store' import { CREATE_WORKSPACE_QUICK_ACTION_ITEM_ID } from './worktree-jump-palette-model' +import type { WorktreeJumpPaletteFilter } from './use-worktree-jump-palette-filter' import type { WorktreeJumpPaletteListEntries } from './use-worktree-jump-palette-list-entries' import type { WorktreeJumpPaletteLocalState } from './use-worktree-jump-palette-local-state' import type { WorktreeJumpPaletteOpenTabs } from './use-worktree-jump-palette-open-tabs' @@ -23,6 +24,7 @@ import type { WorktreeJumpPaletteWorktrees } from './use-worktree-jump-palette-w type WorktreeJumpPaletteSelectionLifecycleInput = WorktreeJumpPaletteStoreState & WorktreeJumpPaletteLocalState & + WorktreeJumpPaletteFilter & WorktreeJumpPaletteOpenTabs & WorktreeJumpPaletteProjectTargets & WorktreeJumpPaletteQuickActions & @@ -57,6 +59,10 @@ export function useWorktreeJumpPaletteSelectionLifecycle({ setQuery, setSelectedItemId, setRawFilter, + filterModel, + workspaceHostScope, + visibleWorkspaceHostIds, + filterRepoIds, selectionMovedByUserRef, taskSourceUrl, listRef, @@ -108,7 +114,13 @@ export function useWorktreeJumpPaletteSelectionLifecycle({ setQuery('') setSelectedItemId('') selectionMovedByUserRef.current = false - setRawFilter(EMPTY_PALETTE_FILTER) + // Each open starts from the sidebar's current Show scope, not the last palette selection. + setRawFilter( + buildPaletteFilterFromSidebarScope( + { workspaceHostScope, visibleWorkspaceHostIds, filterRepoIds }, + filterModel + ) + ) listRef.current?.scrollTo(0, 0) } if (!visible && wasVisibleRef.current) { diff --git a/src/renderer/src/components/use-worktree-jump-palette-store-state.ts b/src/renderer/src/components/use-worktree-jump-palette-store-state.ts index c10778abaa4..c0d6280bdf5 100644 --- a/src/renderer/src/components/use-worktree-jump-palette-store-state.ts +++ b/src/renderer/src/components/use-worktree-jump-palette-store-state.ts @@ -97,6 +97,9 @@ export function useWorktreeJumpPaletteStoreState({ (state) => state.hideWorkspacesFromOtherDevices ) const showSleepingWorkspaces = useAppStore((state) => state.showSleepingWorkspaces) + const workspaceHostScope = useAppStore((state) => state.workspaceHostScope) + const visibleWorkspaceHostIds = useAppStore((state) => state.visibleWorkspaceHostIds) + const filterRepoIds = useAppStore((state) => state.filterRepoIds) const alwaysShowDefaultBranchWorkspace = useAppStore( (state) => state.alwaysShowDefaultBranchWorkspace ) @@ -174,6 +177,9 @@ export function useWorktreeJumpPaletteStoreState({ hideDetachedHeadWorkspaces, hideWorkspacesFromOtherDevices, showSleepingWorkspaces, + workspaceHostScope, + visibleWorkspaceHostIds, + filterRepoIds, alwaysShowDefaultBranchWorkspace, lastVisitedAtByWorktreeId, workspacePortScan,