diff --git a/.gitignore b/.gitignore
index a469ca2b20a..6722fc5ae54 100644
--- a/.gitignore
+++ b/.gitignore
@@ -103,7 +103,6 @@ docs/**
!docs/agent-skill-sharing-implementation-checklist.md
!docs/mobile-terminal-shortcut-bar.md
!docs/reference/
-!docs/reference/cmd-j-ranking.md
!docs/reference/git-compatibility.md
!docs/reference/headless-linux-server.md
!docs/reference/ime-regression-checklist.md
diff --git a/src/renderer/src/components/worktree-jump-palette-primitives.test.tsx b/src/renderer/src/components/worktree-jump-palette-primitives.test.tsx
new file mode 100644
index 00000000000..1d39ab09749
--- /dev/null
+++ b/src/renderer/src/components/worktree-jump-palette-primitives.test.tsx
@@ -0,0 +1,45 @@
+// @vitest-environment happy-dom
+
+import { cleanup, render, screen } from '@testing-library/react'
+import { afterEach, expect, it } from 'vitest'
+import { TooltipProvider } from '@/components/ui/tooltip'
+import { PaletteOpenTabPrimaryLine } from './worktree-jump-palette-primitives'
+
+afterEach(() => cleanup())
+
+function renderPrimaryLine(
+ secondaryMatches: readonly { text: string; ranges: readonly never[] }[]
+): void {
+ render(
+
+
+
+ )
+}
+
+it('exposes the extra secondary matches without adding a palette tab stop', () => {
+ renderPrimaryLine([
+ { text: 'src/app.ts', ranges: [] },
+ { text: 'src/deep/nested.ts', ranges: [] },
+ { text: 'docs/readme.md', ranges: [] }
+ ])
+
+ expect(screen.getByText('src/deep/nested.ts, docs/readme.md')).toBeTruthy()
+ const badge = screen.getByText('+2')
+ expect(badge.getAttribute('aria-hidden')).toBe('true')
+ expect(badge.tabIndex).toBe(-1)
+})
+
+it('renders no badge when every secondary match is already shown', () => {
+ renderPrimaryLine([{ text: 'src/app.ts', ranges: [] }])
+
+ expect(screen.queryByText(/^\+\d+$/)).toBeNull()
+})
diff --git a/src/renderer/src/components/worktree-jump-palette-primitives.tsx b/src/renderer/src/components/worktree-jump-palette-primitives.tsx
index ffdcff0740a..85e8ab04f29 100644
--- a/src/renderer/src/components/worktree-jump-palette-primitives.tsx
+++ b/src/renderer/src/components/worktree-jump-palette-primitives.tsx
@@ -124,27 +124,35 @@ export function PaletteOpenTabPrimaryLine({
>
) : null}
{additionalSecondaryMatches.length ? (
-
-
- match.text).join(', ')}
- className="shrink-0 rounded-md border border-border/60 bg-background/45 px-1.5 py-px text-[9px] font-medium text-muted-foreground/88"
- >
- +{additionalSecondaryMatches.length}
-
-
-
- {additionalSecondaryMatches.map((match) => (
-
-
-
- ))}
-
-
+ <>
+ {/* Tab selects the palette filter, so the badge stays out of the tab order and
+ reads its matches through the row's own accessible name instead. */}
+
+ {additionalSecondaryMatches.map((match) => match.text).join(', ')}
+
+
+
+
+ +{additionalSecondaryMatches.length}
+
+
+
+ {additionalSecondaryMatches.map((match) => (
+
+
+
+ ))}
+
+
+ >
) : null}
{showWorktree ? (
<>
diff --git a/src/renderer/src/lib/browser-workspace-tab-activation.test.ts b/src/renderer/src/lib/browser-workspace-tab-activation.test.ts
new file mode 100644
index 00000000000..f8e351e1170
--- /dev/null
+++ b/src/renderer/src/lib/browser-workspace-tab-activation.test.ts
@@ -0,0 +1,82 @@
+// @vitest-environment happy-dom
+
+import { afterEach, expect, it } from 'vitest'
+import { useAppStore } from '@/store'
+import type { Tab } from '../../../shared/tab-types'
+import type { Worktree } from '../../../shared/worktree/types'
+import { getActivatableBrowserWorkspaceTab } from './browser-workspace-tab-activation'
+
+const initialState = useAppStore.getInitialState()
+afterEach(() => useAppStore.setState(initialState, true))
+
+function makeWorktree(overrides: Partial & Pick): Worktree {
+ return {
+ repoId: 'repo',
+ path: '/workspace',
+ head: '',
+ branch: 'main',
+ isBare: false,
+ isMainWorktree: true,
+ displayName: 'Workspace',
+ comment: '',
+ linkedIssue: null,
+ linkedPR: null,
+ linkedLinearIssue: null,
+ isArchived: false,
+ isUnread: false,
+ isPinned: false,
+ sortOrder: 0,
+ lastActivityAt: 0,
+ ...overrides
+ }
+}
+
+const browserTab: Tab = {
+ id: 'unified-browser',
+ entityId: 'workspace',
+ groupId: 'group',
+ worktreeId: 'wt',
+ contentType: 'browser',
+ label: 'Browser',
+ customLabel: null,
+ color: null,
+ sortOrder: 0,
+ createdAt: 0
+}
+
+function seedState(worktreesByRepo: Record, tab: Tab): void {
+ useAppStore.setState(
+ { ...initialState, worktreesByRepo, unifiedTabsByWorktree: { wt: [tab] } },
+ true
+ )
+}
+
+it('refuses a hostless browser tab for a remote worktree whose ID also exists locally', () => {
+ seedState(
+ {
+ local: [makeWorktree({ id: 'wt' })],
+ remote: [makeWorktree({ id: 'wt', repoId: 'repo-remote', hostId: 'ssh:remote' })]
+ },
+ browserTab
+ )
+
+ expect(
+ getActivatableBrowserWorkspaceTab({
+ worktreeId: 'wt',
+ workspaceId: 'workspace',
+ executionHostId: 'ssh:remote'
+ })
+ ).toBeNull()
+})
+
+it('accepts a hostless browser tab when the worktree ID is unambiguous', () => {
+ seedState({ remote: [makeWorktree({ id: 'wt', hostId: 'ssh:remote' })] }, browserTab)
+
+ expect(
+ getActivatableBrowserWorkspaceTab({
+ worktreeId: 'wt',
+ workspaceId: 'workspace',
+ executionHostId: 'ssh:remote'
+ })
+ ).toEqual(browserTab)
+})
diff --git a/src/renderer/src/lib/browser-workspace-tab-activation.ts b/src/renderer/src/lib/browser-workspace-tab-activation.ts
index 3e82d7c0985..3f5146877b4 100644
--- a/src/renderer/src/lib/browser-workspace-tab-activation.ts
+++ b/src/renderer/src/lib/browser-workspace-tab-activation.ts
@@ -1,7 +1,8 @@
import { useAppStore } from '@/store'
import type { Tab } from '../../../shared/tab-types'
import type { ExecutionHostId } from '../../../shared/execution-host'
-import { isUnifiedTabOwnedByWorktree } from './unified-tab-host-ownership'
+import { getIndexedAllWorktrees } from '@/store/worktree-repo-index'
+import { findAmbiguousWorktreeIds, isUnifiedTabOwnedByWorktree } from './unified-tab-host-ownership'
type BrowserWorkspaceTabTarget = {
worktreeId: string
@@ -18,6 +19,10 @@ export function getActivatableBrowserWorkspaceTab(params: BrowserWorkspaceTabTar
if (params.executionHostId && !worktree) {
return null
}
+ // A hostless tab cannot be attributed when the same worktree ID exists on several hosts.
+ const ambiguousWorktreeIds = findAmbiguousWorktreeIds(
+ getIndexedAllWorktrees(state.worktreesByRepo)
+ )
// setActiveBrowserTab resolves its backing tab globally by workspace ID.
const tabs = Object.values(state.unifiedTabsByWorktree).flat()
const browserTabs = tabs.filter(
@@ -28,7 +33,7 @@ export function getActivatableBrowserWorkspaceTab(params: BrowserWorkspaceTabTar
browserTabs.some(
(tab) =>
tab.worktreeId !== params.worktreeId ||
- (worktree && !isUnifiedTabOwnedByWorktree(tab, worktree, new Set()))
+ (worktree && !isUnifiedTabOwnedByWorktree(tab, worktree, ambiguousWorktreeIds))
) ||
!unifiedTab ||
tabs.filter((candidate) => candidate.id === unifiedTab.id).length !== 1
diff --git a/src/renderer/src/lib/workspace-tab-palette-activation.store.test.ts b/src/renderer/src/lib/workspace-tab-palette-activation.store.test.ts
index 70672e256b6..60b10097f86 100644
--- a/src/renderer/src/lib/workspace-tab-palette-activation.store.test.ts
+++ b/src/renderer/src/lib/workspace-tab-palette-activation.store.test.ts
@@ -81,3 +81,132 @@ it('keeps the selected diff active when an editor for the same file shares its g
expect(useAppStore.getState().groupsByWorktree.wt[0].activeTabId).toBe('diff')
expect(useAppStore.getState().activeFileId).toBe('file')
})
+
+function makeWorktree(overrides: Partial & Pick): Worktree {
+ return {
+ repoId: 'repo',
+ path: '/workspace',
+ head: '',
+ branch: 'main',
+ isBare: false,
+ isMainWorktree: true,
+ displayName: 'Workspace',
+ comment: '',
+ linkedIssue: null,
+ linkedPR: null,
+ linkedLinearIssue: null,
+ isArchived: false,
+ isUnread: false,
+ isPinned: false,
+ sortOrder: 0,
+ lastActivityAt: 0,
+ ...overrides
+ }
+}
+
+const COLLIDING_WORKTREES = {
+ local: [makeWorktree({ id: 'wt' })],
+ remote: [makeWorktree({ id: 'wt', repoId: 'repo-remote', hostId: 'ssh:remote' })]
+}
+
+it('refuses a hostless tab for a remote target whose worktree ID also exists locally', () => {
+ const terminal: Tab = {
+ id: 'unified-terminal',
+ entityId: 'terminal',
+ groupId: 'group',
+ worktreeId: 'wt',
+ contentType: 'terminal',
+ label: 'Terminal',
+ customLabel: null,
+ color: null,
+ sortOrder: 0,
+ createdAt: 0
+ }
+ useAppStore.setState(
+ {
+ ...initialState,
+ worktreesByRepo: COLLIDING_WORKTREES,
+ groupsByWorktree: {
+ wt: [
+ {
+ id: 'group',
+ worktreeId: 'wt',
+ activeTabId: 'unified-terminal',
+ tabOrder: ['unified-terminal']
+ }
+ ]
+ },
+ activeGroupIdByWorktree: { wt: 'group' },
+ unifiedTabsByWorktree: { wt: [terminal] }
+ },
+ true
+ )
+
+ expect(
+ activateWorkspaceTabPaletteResult({
+ worktreeId: 'wt',
+ groupId: 'group',
+ tabId: 'unified-terminal',
+ entityId: 'terminal',
+ contentType: 'terminal',
+ executionHostId: 'ssh:remote'
+ })
+ ).toEqual({ status: 'failed', reason: 'missing-tab' })
+})
+
+it('refuses a hostless backing file for a remote target whose worktree ID also exists locally', () => {
+ const editor: Tab = {
+ id: 'unified-editor',
+ entityId: 'file',
+ groupId: 'group',
+ worktreeId: 'wt',
+ contentType: 'editor',
+ executionHostId: 'ssh:remote',
+ label: 'app.ts',
+ customLabel: null,
+ color: null,
+ sortOrder: 0,
+ createdAt: 0
+ }
+ useAppStore.setState(
+ {
+ ...initialState,
+ worktreesByRepo: COLLIDING_WORKTREES,
+ groupsByWorktree: {
+ wt: [
+ {
+ id: 'group',
+ worktreeId: 'wt',
+ activeTabId: 'unified-editor',
+ tabOrder: ['unified-editor']
+ }
+ ]
+ },
+ activeGroupIdByWorktree: { wt: 'group' },
+ unifiedTabsByWorktree: { wt: [editor] },
+ openFiles: [
+ {
+ id: 'file',
+ worktreeId: 'wt',
+ filePath: '/workspace/app.ts',
+ relativePath: 'app.ts',
+ language: 'typescript',
+ isDirty: false,
+ mode: 'edit'
+ }
+ ]
+ },
+ true
+ )
+
+ expect(
+ activateWorkspaceTabPaletteResult({
+ worktreeId: 'wt',
+ groupId: 'group',
+ tabId: 'unified-editor',
+ entityId: 'file',
+ contentType: 'editor',
+ executionHostId: 'ssh:remote'
+ })
+ ).toEqual({ status: 'failed', reason: 'missing-file' })
+})
diff --git a/src/renderer/src/lib/workspace-tab-palette-activation.ts b/src/renderer/src/lib/workspace-tab-palette-activation.ts
index 4e52d8077f9..810e5f13d4c 100644
--- a/src/renderer/src/lib/workspace-tab-palette-activation.ts
+++ b/src/renderer/src/lib/workspace-tab-palette-activation.ts
@@ -8,7 +8,9 @@ import { useAppStore } from '@/store'
import type { AppState } from '@/store/types'
import type { ExecutionHostId } from '../../../shared/execution-host'
import { activateAndRevealWorktree } from './worktree-activation'
+import { getIndexedAllWorktrees } from '@/store/worktree-repo-index'
import {
+ findAmbiguousWorktreeIds,
isOpenFileOwnedByWorktree,
isUnifiedTabOwnedByWorktree
} from './unified-tab-host-ownership'
@@ -41,6 +43,7 @@ type WorkspaceTabPaletteActivationState = Pick<
| 'setActiveTab'
| 'setActiveTabType'
| 'unifiedTabsByWorktree'
+ | 'worktreesByRepo'
>
function validateTarget(
@@ -51,6 +54,10 @@ function validateTarget(
if (!worktree) {
return 'missing-worktree'
}
+ // A hostless record cannot be attributed when the same worktree ID exists on several hosts.
+ const ambiguousWorktreeIds = findAmbiguousWorktreeIds(
+ getIndexedAllWorktrees(state.worktreesByRepo)
+ )
const group = (state.groupsByWorktree[result.worktreeId] ?? []).find(
(candidate) => candidate.id === result.groupId
)
@@ -66,7 +73,7 @@ function validateTarget(
candidate.groupId === result.groupId &&
candidate.worktreeId === result.worktreeId &&
candidate.contentType === result.contentType &&
- isUnifiedTabOwnedByWorktree(candidate, worktree, new Set())
+ isUnifiedTabOwnedByWorktree(candidate, worktree, ambiguousWorktreeIds)
)
if (tabs.length !== 1 || !tab) {
return 'missing-tab'
@@ -77,9 +84,12 @@ function validateTarget(
return 'missing-file'
}
const file = files[0]
- const hasExplicitHost =
+ const hasExplicitHost = Boolean(
file.operationProvenance || file.externalSshTargetId || file.runtimeEnvironmentId
- if (hasExplicitHost && !isOpenFileOwnedByWorktree(file, worktree)) {
+ )
+ // A hostless file falls back to local ownership, which only decides the match when IDs collide.
+ const requiresOwnershipCheck = hasExplicitHost || ambiguousWorktreeIds.has(worktree.id)
+ if (requiresOwnershipCheck && !isOpenFileOwnedByWorktree(file, worktree)) {
return 'missing-file'
}
}