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,