fix(cmd-j): align badges with filter ownership

This commit is contained in:
Jinwoo-H
2026-08-23 18:33:01 -04:00
parent c058fb5eac
commit 3d84bde900
4 changed files with 55 additions and 42 deletions
@@ -230,6 +230,10 @@ describe('WorktreeJumpPalette', () => {
it('renders the owning host badge on worktree and tab rows', async () => {
await renderPalette({
...makeRecentTabState(),
worktreesByRepo: {
...makeRecentTabState().worktreesByRepo,
folder: [makeWorktree('folder', 'Folder', { repoId: 'folder', hostId: 'ssh:ssh-1' })]
},
repos: [makeRepo(), { ...makeRepo(), id: 'ssh-repo', connectionId: 'ssh-1' }],
sshTargetLabels: new Map([['ssh-1', 'Builder']]),
sshConnectionStates: new Map([
@@ -238,7 +242,9 @@ describe('WorktreeJumpPalette', () => {
})
const badge = `[aria-label="Host: ${getExecutionHostLabel('local')}"]`
expect(testContainer.querySelector(`[data-command-item*="worktree:"] ${badge}`)).not.toBeNull()
expect(
testContainer.querySelector('[data-command-item*="folder"] [aria-label="Host: Builder"]')
).not.toBeNull()
expect(
testContainer.querySelector(`[data-command-item*="workspace-tab:"] ${badge}`)
).not.toBeNull()
@@ -154,7 +154,11 @@ import { buildPaletteListEntryRenderKeys } from '@/components/cmd-j/palette-list
import { formatPaletteSessionAge } from '@/components/cmd-j/palette-session-age'
import PaletteFilterMenu from '@/components/cmd-j/PaletteFilterMenu'
import PaletteFilterChips from '@/components/cmd-j/PaletteFilterChips'
import { buildPaletteFilterModel } from '@/components/cmd-j/palette-filter-options'
import {
buildPaletteFilterModel,
resolveRepoFilterHostId,
resolveWorktreeFilterHostId
} from '@/components/cmd-j/palette-filter-options'
import { getProjectGroupExecutionHostIdForRows } from '@/components/sidebar/worktree-list/listing/host-filtering'
import {
buildPaletteFilterPredicate,
@@ -895,6 +899,17 @@ function WorktreeJumpPaletteContent({
}, [filterModel])
const filterActive = isPaletteFilterActive(filter)
const hostFilterActive = filter.hostIds.length > 0
const getWorktreeHostBadge = useCallback(
(worktree: Worktree | undefined, fallbackHostId?: ExecutionHostId) =>
getPaletteHostBadge(
worktree
? resolveWorktreeFilterHostId(worktree, filterModel.hostIdByRepoId, defaultHostId)
: fallbackHostId,
hostOptions,
hostFilterActive
),
[defaultHostId, filterModel.hostIdByRepoId, hostFilterActive, hostOptions]
)
const filterPredicate = useMemo(
() => buildPaletteFilterPredicate(filter, filterModel),
[filter, filterModel]
@@ -1102,17 +1117,13 @@ function WorktreeJumpPaletteContent({
const hostLabelByWorktreeId = useMemo(() => {
const labels = new Map<string, string>()
for (const worktree of allWorktrees) {
const badge = getPaletteHostBadge(
resolveRepoForWorktree(worktree),
hostOptions,
hostFilterActive
)
const badge = getWorktreeHostBadge(worktree)
if (badge) {
labels.set(getWorktreeHostIdentity(worktree), badge.label)
}
}
return labels
}, [allWorktrees, hostFilterActive, hostOptions, resolveRepoForWorktree])
}, [allWorktrees, getWorktreeHostBadge])
// Why keyed on the unsorted list: normalized documents depend only on text
// inputs, so re-sorting for recency re-ranks without re-normalizing anything.
@@ -3222,7 +3233,7 @@ function WorktreeJumpPaletteContent({
: null
const isSshDisconnected = sshStatus != null && sshStatus !== 'connected'
const sessionAge = formatPaletteSessionAge(worktree.lastActivityAt, paletteNowMs)
const hostBadge = getPaletteHostBadge(repo, hostOptions, hostFilterActive)
const hostBadge = getWorktreeHostBadge(worktree)
return (
<CommandItem
key={renderKey}
@@ -3354,7 +3365,15 @@ function WorktreeJumpPaletteContent({
const result = entry.result
const isProject = result.kind === 'project'
const hostBadge = isProject
? getPaletteHostBadge(result.repo, hostOptions, hostFilterActive)
? getPaletteHostBadge(
resolveRepoFilterHostId(
result.repo.id,
filterModel.hostIdByRepoId,
defaultHostId
),
hostOptions,
hostFilterActive
)
: null
const badgeLabel = isProject
? translate('auto.components.WorktreeJumpPalette.projectBadge', 'Project')
@@ -3441,10 +3460,9 @@ function WorktreeJumpPaletteContent({
? resolveRepoForWorktree(workspaceTabWorktree)
: undefined
const workspaceTabRepoName = workspaceTabRepo?.displayName ?? result.repoName
const workspaceTabHostBadge = getPaletteHostBadge(
workspaceTabRepo,
hostOptions,
hostFilterActive
const workspaceTabHostBadge = getWorktreeHostBadge(
workspaceTabWorktree,
result.executionHostId
)
const workspaceTabFallback =
result.contentType === 'terminal' && result.occupantAgent ? (
@@ -3542,10 +3560,9 @@ function WorktreeJumpPaletteContent({
? resolveRepoForWorktree(simulatorWorktree)
: undefined
const simulatorRepoName = simulatorRepo?.displayName ?? result.repoName
const simulatorHostBadge = getPaletteHostBadge(
simulatorRepo,
hostOptions,
hostFilterActive
const simulatorHostBadge = getWorktreeHostBadge(
simulatorWorktree,
result.executionHostId
)
const sessionAge = formatPaletteSessionAge(
result.lastActiveAt ?? null,
@@ -3629,11 +3646,7 @@ function WorktreeJumpPaletteContent({
? resolveRepoForWorktree(browserWorktree)
: undefined
const browserRepoName = browserRepo?.displayName ?? result.repoName
const browserHostBadge = getPaletteHostBadge(
browserRepo,
hostOptions,
hostFilterActive
)
const browserHostBadge = getWorktreeHostBadge(browserWorktree, result.executionHostId)
const sessionAge = formatPaletteSessionAge(result.lastActiveAt ?? null, paletteNowMs)
return (
@@ -20,7 +20,7 @@ describe('getPaletteHostBadge', () => {
settings: { activeRuntimeEnvironmentId: null }
})
expect(getPaletteHostBadge({ connectionId: null }, hosts)).toBeNull()
expect(getPaletteHostBadge('local', hosts)).toBeNull()
})
it('returns null when the only non-local host is configured but disconnected', () => {
@@ -32,7 +32,7 @@ describe('getPaletteHostBadge', () => {
// No connection state -> the SSH host is 'disconnected', so there's nothing
// live to disambiguate from and rows stay badge-free.
expect(getPaletteHostBadge({ connectionId: null }, hosts)).toBeNull()
expect(getPaletteHostBadge('local', hosts)).toBeNull()
})
it('badges a disconnected host anyway when a host filter is applied', () => {
@@ -44,7 +44,7 @@ describe('getPaletteHostBadge', () => {
// Why: with a host filter on, the badge is the only thing explaining which
// rows survived, so liveness must not suppress it.
expect(getPaletteHostBadge({ connectionId: 'ssh-1' }, hosts, true)).toEqual({
expect(getPaletteHostBadge('ssh:ssh-1', hosts, true)).toEqual({
hostId: 'ssh:ssh-1',
label: 'Builder'
})
@@ -58,7 +58,7 @@ describe('getPaletteHostBadge', () => {
settings: { activeRuntimeEnvironmentId: null }
})
expect(getPaletteHostBadge({ connectionId: null }, hosts)).toEqual({
expect(getPaletteHostBadge('local', hosts)).toEqual({
hostId: 'local',
label: LOCAL_HOST_LABEL
})
@@ -72,7 +72,7 @@ describe('getPaletteHostBadge', () => {
settings: { activeRuntimeEnvironmentId: null }
})
expect(getPaletteHostBadge({ connectionId: 'ssh-1' }, hosts)).toEqual({
expect(getPaletteHostBadge('ssh:ssh-1', hosts)).toEqual({
hostId: 'ssh:ssh-1',
label: 'Builder'
})
@@ -104,7 +104,7 @@ describe('getPaletteHostBadge', () => {
])
})
expect(getPaletteHostBadge({ executionHostId: 'runtime:env-1' }, hosts)).toEqual({
expect(getPaletteHostBadge('runtime:env-1', hosts)).toEqual({
hostId: 'runtime:env-1',
label: 'env-1'
})
@@ -117,10 +117,10 @@ describe('getPaletteHostBadge', () => {
settings: { activeRuntimeEnvironmentId: null }
})
expect(getPaletteHostBadge({ connectionId: null }, hosts)).toBeNull()
expect(getPaletteHostBadge('local', hosts)).toBeNull()
})
it('maps repos with no executionHostId/connectionId to local', () => {
it('uses the local host label for local identities', () => {
const hosts = buildSidebarHostOptions({
repos: [{ connectionId: 'ssh-1' }],
sshTargetLabels: new Map([['ssh-1', 'Builder']]),
@@ -128,13 +128,13 @@ describe('getPaletteHostBadge', () => {
settings: { activeRuntimeEnvironmentId: null }
})
expect(getPaletteHostBadge({}, hosts)).toEqual({
expect(getPaletteHostBadge('local', hosts)).toEqual({
hostId: 'local',
label: LOCAL_HOST_LABEL
})
})
it('returns null when the repo is missing', () => {
it('returns null when the host identity is missing', () => {
const hosts = buildSidebarHostOptions({
repos: [{ connectionId: 'ssh-1' }],
sshTargetLabels: new Map([['ssh-1', 'Builder']]),
@@ -1,9 +1,4 @@
import type { Repo } from '../../../../shared/repo-types'
import {
getRepoExecutionHostId,
LOCAL_EXECUTION_HOST_ID,
type ExecutionHostId
} from '../../../../shared/execution-host'
import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../../shared/execution-host'
import type { SidebarHostOption } from '../sidebar/sidebar-host-options'
export type PaletteHostBadge = {
@@ -22,16 +17,15 @@ function hasActiveRemoteHost(hostOptions: readonly SidebarHostOption[]): boolean
}
export function getPaletteHostBadge(
repo: Pick<Repo, 'connectionId' | 'executionHostId'> | null | undefined,
hostId: ExecutionHostId | null | undefined,
hostOptions: readonly SidebarHostOption[],
// Why: with a host filter applied the badge is the only thing explaining which
// rows survived, so it must show even when every remote is disconnected.
alwaysShowHostLabel = false
): PaletteHostBadge | null {
if (!repo || (!alwaysShowHostLabel && !hasActiveRemoteHost(hostOptions))) {
if (!hostId || (!alwaysShowHostLabel && !hasActiveRemoteHost(hostOptions))) {
return null
}
const hostId = getRepoExecutionHostId(repo)
const host = hostOptions.find((option) => option.id === hostId)
if (!host) {
return null