fix tab search host routing and churn

This commit is contained in:
Jinjing
2026-08-08 12:43:45 -07:00
parent 04f7123d26
commit 705b33f6d3
16 changed files with 215 additions and 47 deletions
@@ -67,6 +67,7 @@ import TabBarCreateEntry from './TabBarCreateEntry'
function terminalResult(overrides: Partial<OpenTabSearchResult> = {}): OpenTabSearchResult {
return {
executionHostId: 'local',
source: 'workspace',
id: 'open-tab:workspace:tab-1',
title: 'Add tab search and jump in worktree',
@@ -14,6 +14,7 @@ function editorTab(
relativePath: string | null
): Extract<OpenTabSearchResult, { source: 'workspace' }> {
return {
executionHostId: 'local',
source: 'workspace',
id: `open-tab:workspace:tab-${relativePath ?? 'none'}`,
title: 'zebra.ts',
@@ -70,6 +71,7 @@ describe('dropFileEntriesCoveredByTabResults', () => {
const results: OpenTabSearchResult[] = [
{ ...editorTab(null), contentType: 'terminal' },
{
executionHostId: 'local',
source: 'browser',
id: 'open-tab:browser:page-1',
title: 'zebra',
@@ -80,6 +82,7 @@ describe('dropFileEntriesCoveredByTabResults', () => {
workspaceId: 'ws-1'
},
{
executionHostId: 'local',
source: 'simulator',
id: 'open-tab:simulator:tab-2',
title: 'zebra',
@@ -13,6 +13,12 @@ import {
type SearchableWorkspaceTab
} from '@/lib/workspace-tab-palette-search'
import type { AppState } from '@/store/types'
import {
getRepoExecutionHostId,
getWorktreeExecutionHostId,
LOCAL_EXECUTION_HOST_ID,
type ExecutionHostId
} from '../../../../shared/execution-host'
export type OpenTabSearchEntries = {
workspaceTabs: readonly SearchableWorkspaceTab[]
@@ -31,35 +37,51 @@ export type OpenTabSearchEntryState = Pick<
| 'activeTabType'
| 'activeTabTypeByWorktree'
| 'activeWorktreeId'
| 'agentStatusByPaneKey'
| 'browserPagesByWorkspace'
| 'browserTabsByWorktree'
| 'groupsByWorktree'
| 'openFiles'
| 'retainedAgentsByPaneKey'
| 'sleepingAgentSessionsByPaneKey'
| 'tabsByWorktree'
| 'unifiedTabsByWorktree'
> & {
executionHostId: ExecutionHostId
generatedTitlesEnabled: boolean
repo: Pick<Repo, 'displayName' | 'id'> | null
worktree: Worktree | null
repo: Pick<Repo, 'connectionId' | 'displayName' | 'executionHostId' | 'id'> | null
worktree: Worktree
}
const EMPTY_ENTRIES: OpenTabSearchEntries = {
workspaceTabs: [],
browserPages: [],
simulatorTabs: []
}
export type OpenTabSearchAgentState = Pick<
AppState,
'agentStatusByPaneKey' | 'retainedAgentsByPaneKey' | 'sleepingAgentSessionsByPaneKey'
>
// No group id: every tab of the worktree is offered, including the one the
// column already shows, matching how Cmd+J lists the tab you are on.
export function selectOpenTabSearchEntryState(
state: AppState,
worktreeId: string
): OpenTabSearchEntryState {
): OpenTabSearchEntryState | null {
const preferredHostId =
state.activeWorktreeId === worktreeId
? (state.activeWorkspaceExecutionHostId ?? LOCAL_EXECUTION_HOST_ID)
: undefined
// Why getKnownWorktreeById: folder workspaces are absent from worktreesByRepo.
const worktree = state.getKnownWorktreeById(worktreeId) ?? null
const worktree = state.getKnownWorktreeById(worktreeId, preferredHostId) ?? null
if (!worktree) {
return null
}
const repoCandidates = state.repos.filter((candidate) => candidate.id === worktree.repoId)
const repo =
(preferredHostId
? repoCandidates.find((candidate) => getRepoExecutionHostId(candidate) === preferredHostId)
: undefined) ??
repoCandidates[0] ??
null
const executionHostId = getWorktreeExecutionHostId(
worktree,
repo ?? undefined,
preferredHostId ?? LOCAL_EXECUTION_HOST_ID
)
return {
activeBrowserTabId: state.activeBrowserTabId,
activeFileId: state.activeFileId,
@@ -70,33 +92,40 @@ export function selectOpenTabSearchEntryState(
activeTabType: state.activeTabType,
activeTabTypeByWorktree: state.activeTabTypeByWorktree,
activeWorktreeId: state.activeWorktreeId,
agentStatusByPaneKey: state.agentStatusByPaneKey,
browserPagesByWorkspace: state.browserPagesByWorkspace,
browserTabsByWorktree: state.browserTabsByWorktree,
executionHostId,
generatedTitlesEnabled: state.settings?.tabAutoGenerateTitle === true,
groupsByWorktree: state.groupsByWorktree,
openFiles: state.openFiles,
repo: worktree
? (state.repos.find((candidate) => candidate.id === worktree.repoId) ?? null)
: null,
retainedAgentsByPaneKey: state.retainedAgentsByPaneKey,
sleepingAgentSessionsByPaneKey: state.sleepingAgentSessionsByPaneKey,
repo,
tabsByWorktree: state.tabsByWorktree,
unifiedTabsByWorktree: state.unifiedTabsByWorktree,
worktree
}
}
export function buildOpenTabSearchEntries(state: OpenTabSearchEntryState): OpenTabSearchEntries {
if (!state.worktree) {
return EMPTY_ENTRIES
export function selectOpenTabSearchAgentState(state: AppState): OpenTabSearchAgentState {
return {
agentStatusByPaneKey: state.agentStatusByPaneKey,
retainedAgentsByPaneKey: state.retainedAgentsByPaneKey,
sleepingAgentSessionsByPaneKey: state.sleepingAgentSessionsByPaneKey
}
}
export function buildOpenTabSearchEntries(
state: OpenTabSearchEntryState,
agentState: OpenTabSearchAgentState
): OpenTabSearchEntries {
const { repo, worktree } = state
const worktrees = [worktree]
const scopedWorktree =
worktree.hostId === state.executionHostId
? worktree
: { ...worktree, hostId: state.executionHostId }
const worktrees = [scopedWorktree]
const scope = {
worktrees,
repoMap: new Map(repo ? [[repo.id, { displayName: repo.displayName }]] : []),
repoMap: new Map(repo ? [[repo.id, repo]] : []),
worktreeOrder: new Map([[worktree.id, 0]])
}
@@ -106,9 +135,9 @@ export function buildOpenTabSearchEntries(state: OpenTabSearchEntryState): OpenT
unifiedTabsByWorktree: state.unifiedTabsByWorktree,
tabsByWorktree: state.tabsByWorktree,
openFiles: state.openFiles,
agentStatusByPaneKey: state.agentStatusByPaneKey,
retainedAgentsByPaneKey: state.retainedAgentsByPaneKey,
sleepingAgentSessionsByPaneKey: state.sleepingAgentSessionsByPaneKey,
agentStatusByPaneKey: agentState.agentStatusByPaneKey,
retainedAgentsByPaneKey: agentState.retainedAgentsByPaneKey,
sleepingAgentSessionsByPaneKey: agentState.sleepingAgentSessionsByPaneKey,
activeGroupIdByWorktree: state.activeGroupIdByWorktree,
groupsByWorktree: state.groupsByWorktree,
activeWorktreeId: state.activeWorktreeId,
@@ -2,6 +2,7 @@
// omnibox. Pure: no store, no React.
import { isClipboardTextByteLengthOverLimit } from '../../../../shared/clipboard-text'
import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../../shared/execution-host'
import {
searchBrowserPages,
type BrowserPaletteSearchResult,
@@ -27,6 +28,7 @@ export const OPEN_TAB_SEARCH_QUERY_MAX_BYTES = 2 * 1024
export type OpenTabSearchSource = 'workspace' | 'browser' | 'simulator'
type OpenTabSearchResultBase = {
executionHostId: ExecutionHostId
/** Stable across renders, so selection survives the deferred query. */
id: string
title: string
@@ -128,9 +130,11 @@ function getEditorRelativePath(entry: SearchableWorkspaceTab | undefined): strin
function baseResult(
source: OpenTabSearchSource,
id: string,
result: EngineResult
result: EngineResult,
executionHostId: ExecutionHostId
): OpenTabSearchResultBase {
return {
executionHostId,
id: `open-tab:${source}:${id}`,
title: result.title,
matchedText: getMatchedText(result),
@@ -165,12 +169,22 @@ export function searchOpenTabs({
}
const workspaceEntriesByTabId = new Map(workspaceTabs.map((entry) => [entry.tab.id, entry]))
const browserEntriesByPageId = new Map(browserPages.map((entry) => [entry.page.id, entry]))
const simulatorEntriesByTabId = new Map(simulatorTabs.map((entry) => [entry.tab.id, entry]))
const entryHostId = (
entry: SearchableWorkspaceTab | SearchableBrowserPage | SearchableSimulatorTab | undefined
): ExecutionHostId => entry?.worktree.hostId ?? LOCAL_EXECUTION_HOST_ID
return [
// Why no isCurrentTab filter: Cmd+J lists the tab you are on, and hiding it
// made the omnibox look broken when you searched for the tab on screen.
...rank('workspace', searchWorkspaceTabs([...workspaceTabs], trimmed), (result) => ({
...baseResult('workspace', result.tabId, result),
...baseResult(
'workspace',
result.tabId,
result,
entryHostId(workspaceEntriesByTabId.get(result.tabId))
),
source: 'workspace',
contentType: result.contentType,
tabId: result.tabId,
@@ -179,14 +193,24 @@ export function searchOpenTabs({
relativePath: getEditorRelativePath(workspaceEntriesByTabId.get(result.tabId))
})),
...rank('browser', searchBrowserPages([...browserPages], trimmed), (result) => ({
...baseResult('browser', result.pageId, result),
...baseResult(
'browser',
result.pageId,
result,
entryHostId(browserEntriesByPageId.get(result.pageId))
),
source: 'browser',
contentType: 'browser',
pageId: result.pageId,
workspaceId: result.workspaceId
})),
...rank('simulator', searchSimulatorTabs([...simulatorTabs], trimmed), (result) => ({
...baseResult('simulator', result.tabId, result),
...baseResult(
'simulator',
result.tabId,
result,
entryHostId(simulatorEntriesByTabId.get(result.tabId))
),
source: 'simulator',
contentType: 'simulator',
tabId: result.tabId,
@@ -30,7 +30,8 @@ vi.mock('@/components/browser-pane/browser-focus', () => ({
import { activateOpenTabSearchResult } from './open-tab-selection-routing'
const terminalResult: OpenTabSearchResult = {
const terminalResult: Extract<OpenTabSearchResult, { source: 'workspace' }> = {
executionHostId: 'runtime:host-1',
source: 'workspace',
id: 'open-tab:workspace:tab-1',
title: 'Claude Code',
@@ -53,6 +54,7 @@ const editorResult: OpenTabSearchResult = {
}
const browserResult: OpenTabSearchResult = {
executionHostId: 'runtime:host-1',
source: 'browser',
id: 'open-tab:browser:page-1',
title: 'Project Docs',
@@ -64,6 +66,7 @@ const browserResult: OpenTabSearchResult = {
}
const simulatorResult: OpenTabSearchResult = {
executionHostId: 'runtime:host-1',
source: 'simulator',
id: 'open-tab:simulator:tab-3',
title: 'iPhone 15',
@@ -92,6 +95,7 @@ describe('activateOpenTabSearchResult', () => {
expect(mocks.activateWorkspaceTab).toHaveBeenCalledWith({
contentType: 'terminal',
entityId: 'term-1',
executionHostId: 'runtime:host-1',
groupId: 'group-2',
tabId: 'tab-1',
worktreeId: 'wt-1'
@@ -112,6 +116,7 @@ describe('activateOpenTabSearchResult', () => {
it('carries the activation focus target into the browser focus request', () => {
const outcome = activateOpenTabSearchResult(browserResult)
expect(mocks.activateBrowserPage).toHaveBeenCalledWith({
executionHostId: 'runtime:host-1',
pageId: 'page-1',
workspaceId: 'ws-1',
worktreeId: 'wt-1'
@@ -140,7 +145,11 @@ describe('activateOpenTabSearchResult', () => {
throw new Error('expected activation')
}
outcome.focus?.()
expect(mocks.activateSimulatorTab).toHaveBeenCalledWith({ tabId: 'tab-3', worktreeId: 'wt-1' })
expect(mocks.activateSimulatorTab).toHaveBeenCalledWith({
executionHostId: 'runtime:host-1',
tabId: 'tab-3',
worktreeId: 'wt-1'
})
expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith('tab-3')
})
@@ -40,6 +40,7 @@ function requestBrowserPageFocus(detail: BrowserFocusRequestDetail): void {
export function activateOpenTabSearchResult(result: OpenTabSearchResult): OpenTabSelectionOutcome {
if (result.source === 'browser') {
const activation = activateBrowserPagePaletteResult({
executionHostId: result.executionHostId,
pageId: result.pageId,
workspaceId: result.workspaceId,
worktreeId: result.worktreeId
@@ -62,6 +63,7 @@ export function activateOpenTabSearchResult(result: OpenTabSearchResult): OpenTa
if (result.source === 'simulator') {
const activation = activateSimulatorTabPaletteResult({
executionHostId: result.executionHostId,
tabId: result.tabId,
worktreeId: result.worktreeId
})
@@ -78,6 +80,7 @@ export function activateOpenTabSearchResult(result: OpenTabSearchResult): OpenTa
}
const activation = activateWorkspaceTabPaletteResult({
executionHostId: result.executionHostId,
contentType: result.contentType,
entityId: result.entityId,
groupId: result.groupId,
@@ -237,6 +237,41 @@ describe('useOpenTabSearch', () => {
expect(result.current.every((entry) => entry.worktreeId === 'wt-1')).toBe(true)
})
it('keeps the active runtime host when worktree ids collide', () => {
const runtimeHost = 'runtime:host-1' as const
const localWorktree = { ...makeWorktree('wt-1', 'Local'), hostId: 'local' as const }
const runtimeWorktree = {
...makeWorktree('wt-1', 'Runtime'),
hostId: runtimeHost,
path: '/runtime/wt-1'
}
seedStore({
activeWorkspaceExecutionHostId: runtimeHost,
repos: [
{ ...repo, executionHostId: 'local', path: '/local/repo-1' },
{ ...repo, executionHostId: runtimeHost, path: '/runtime/repo-1' }
],
worktreesByRepo: { 'repo-1': [localWorktree, runtimeWorktree] }
})
const { result } = renderSearch()
expect(result.current).not.toHaveLength(0)
expect(result.current.every((entry) => entry.executionHostId === runtimeHost)).toBe(true)
})
it('does not rebuild results for agent-status heartbeats while open', () => {
const { result } = renderSearch()
const initialResults = result.current
const state = useAppStore.getState()
act(() => {
useAppStore.setState({ agentStatusByPaneKey: { ...state.agentStatusByPaneKey } })
})
expect(result.current).toBe(initialResults)
})
it('includes tabs from every column of the worktree, not just the focused one', () => {
const { result } = renderSearch()
@@ -3,7 +3,11 @@
import { useDeferredValue, useMemo } from 'react'
import { useShallow } from 'zustand/react/shallow'
import { useAppStore } from '@/store'
import { buildOpenTabSearchEntries, selectOpenTabSearchEntryState } from './open-tab-search-entries'
import {
buildOpenTabSearchEntries,
selectOpenTabSearchAgentState,
selectOpenTabSearchEntryState
} from './open-tab-search-entries'
import { searchOpenTabs, type OpenTabSearchResult } from './open-tab-search'
const EMPTY_RESULTS: OpenTabSearchResult[] = []
@@ -23,7 +27,17 @@ export function useOpenTabSearch({
const state = useAppStore(
useShallow((store) => (enabled ? selectOpenTabSearchEntryState(store, worktreeId) : null))
)
const entries = useMemo(() => (state ? buildOpenTabSearchEntries(state) : null), [state])
// Why snapshot: agent status is a high-frequency stream; tab search metadata
// stays stable while the menu is open and refreshes when its tab set changes.
const agentState = useMemo(
() => (enabled ? selectOpenTabSearchAgentState(useAppStore.getState()) : null),
// oxlint-disable-next-line react-hooks/exhaustive-deps -- Refresh on open or tab-set changes, never agent-status churn.
[enabled, state?.tabsByWorktree, state?.unifiedTabsByWorktree, worktreeId]
)
const entries = useMemo(
() => (state && agentState ? buildOpenTabSearchEntries(state, agentState) : null),
[agentState, state]
)
const deferredQuery = useDeferredValue(query)
return useMemo(
+8 -1
View File
@@ -2987,7 +2987,13 @@
"d62d63b807": "Crear archivo",
"25dc1cd653": "Abrir archivo",
"7cdf8ee0c8": "Abrir URL",
"b27864279e": "Iniciar agente"
"b27864279e": "Iniciar agente",
"0e5b7a3f16": "Buscar pestañas abiertas, archivos, URL y agentes…",
"8f0a1c4d92": "Cambiar a la pestaña",
"2c38630a01": "El espacio de trabajo ya no existe",
"4f0d9a71c2": "La pestaña ya no existe",
"d7d496a451": "La página del navegador ya no existe",
"7726ce9970": "La pestaña del emulador móvil ya no existe"
},
"TabBarQuickCommandsButton": {
"a2c7a33831": "Comando",
@@ -3021,6 +3027,7 @@
"classifier": {
"42e6262ae9": "No hay acción disponible.",
"097a982ee0": "Cargando archivos...",
"c41f8d20b7": "Buscar pestañas abiertas, archivos, URL y agentes…",
"90eb94dc48": "Introduce una URL http:// o https://.",
"5553b283ce": "Introduce una URL o ruta de archivo.",
"queryTooLarge": "El texto de búsqueda es demasiado grande.",
+8 -1
View File
@@ -2987,7 +2987,13 @@
"d62d63b807": "ファイルの作成",
"25dc1cd653": "ファイルを開く",
"7cdf8ee0c8": "URLを開く",
"b27864279e": "agent を起動"
"b27864279e": "agent を起動",
"0e5b7a3f16": "開いているタブ、ファイル、URL、agent を検索…",
"8f0a1c4d92": "タブに切り替える",
"2c38630a01": "ワークスペースは存在しません",
"4f0d9a71c2": "タブは存在しません",
"d7d496a451": "ブラウザページは存在しません",
"7726ce9970": "モバイルエミュレータのタブは存在しません"
},
"TabBarQuickCommandsButton": {
"a2c7a33831": "コマンド",
@@ -3021,6 +3027,7 @@
"classifier": {
"42e6262ae9": "利用可能な操作はありません。",
"097a982ee0": "ファイルをロード中...",
"c41f8d20b7": "開いているタブ、ファイル、URL、agent を検索…",
"90eb94dc48": "http:// または https:// URL を入力します。",
"5553b283ce": "URL またはファイル パスを入力します。",
"queryTooLarge": "検索テキストが大きすぎます。",
+8 -1
View File
@@ -2987,7 +2987,13 @@
"d62d63b807": "파일 생성",
"25dc1cd653": "파일 열기",
"7cdf8ee0c8": "URL 열기",
"b27864279e": "agent 실행"
"b27864279e": "agent 실행",
"0e5b7a3f16": "열린 탭, 파일, URL, agent 검색…",
"8f0a1c4d92": "탭으로 전환",
"2c38630a01": "워크스페이스가 더 이상 존재하지 않습니다",
"4f0d9a71c2": "탭이 더 이상 존재하지 않습니다",
"d7d496a451": "브라우저 페이지가 더 이상 존재하지 않습니다",
"7726ce9970": "모바일 에뮬레이터 탭이 더 이상 존재하지 않습니다"
},
"TabBarQuickCommandsButton": {
"a2c7a33831": "명령",
@@ -3021,6 +3027,7 @@
"classifier": {
"42e6262ae9": "사용할 수 있는 작업이 없습니다.",
"097a982ee0": "파일 로드 중...",
"c41f8d20b7": "열린 탭, 파일, URL, agent 검색…",
"90eb94dc48": "http:// 또는 https:// URL을 입력하세요.",
"5553b283ce": "URL 또는 파일 경로를 입력하세요.",
"queryTooLarge": "검색 텍스트가 너무 큽니다.",
+8 -1
View File
@@ -2999,7 +2999,13 @@
"d62d63b807": "创建文件",
"25dc1cd653": "打开文件",
"7cdf8ee0c8": "打开网址",
"b27864279e": "启动智能体"
"b27864279e": "启动智能体",
"0e5b7a3f16": "搜索打开的标签页、文件、URL 和智能体…",
"8f0a1c4d92": "切换到标签页",
"2c38630a01": "工作区已不存在",
"4f0d9a71c2": "标签页已不存在",
"d7d496a451": "浏览器页面已不存在",
"7726ce9970": "移动模拟器标签页已不存在"
},
"TabBarQuickCommandsButton": {
"a2c7a33831": "命令",
@@ -3033,6 +3039,7 @@
"classifier": {
"42e6262ae9": "没有可用的操作。",
"097a982ee0": "正在加载文件...",
"c41f8d20b7": "搜索打开的标签页、文件、URL 和智能体…",
"90eb94dc48": "输入 http:// 或 https:// URL。",
"5553b283ce": "输入 URL 或文件路径。",
"queryTooLarge": "搜索文本过长。",
@@ -1,4 +1,5 @@
import { useAppStore } from '@/store'
import type { ExecutionHostId } from '../../../shared/execution-host'
import { isBlankBrowserUrl } from './browser-palette-search'
import { activateAndRevealWorktree } from './worktree-activation'
@@ -11,12 +12,14 @@ export type BrowserPagePaletteActivationResult =
| { status: 'failed'; reason: BrowserPagePaletteActivationFailure }
export type BrowserPagePaletteActivationTarget = {
executionHostId?: ExecutionHostId
pageId: string
workspaceId: string
worktreeId: string
}
export function activateBrowserPagePaletteResult({
executionHostId,
pageId,
workspaceId,
worktreeId
@@ -28,7 +31,7 @@ export function activateBrowserPagePaletteResult({
const workspace = (initialState.browserTabsByWorktree[worktreeId] ?? []).find(
(candidate) => candidate.id === workspaceId
)
const worktree = initialState.getKnownWorktreeById(worktreeId)
const worktree = initialState.getKnownWorktreeById(worktreeId, executionHostId)
if (!page || !workspace || !worktree) {
return { status: 'failed', reason: 'missing-page' }
}
@@ -39,9 +42,10 @@ export function activateBrowserPagePaletteResult({
? 'address-bar'
: 'webview'
const targetHostId = executionHostId ?? worktree.hostId
const activated = activateAndRevealWorktree(
worktree.id,
worktree.hostId ? { executionHostId: worktree.hostId } : {}
targetHostId ? { executionHostId: targetHostId } : {}
)
if (!activated) {
return { status: 'failed', reason: 'missing-worktree' }
@@ -1,4 +1,5 @@
import { useAppStore } from '@/store'
import type { ExecutionHostId } from '../../../shared/execution-host'
import { activateAndRevealWorktree } from './worktree-activation'
export type SimulatorTabPaletteActivationFailure = 'missing-tab' | 'missing-worktree'
@@ -8,11 +9,13 @@ export type SimulatorTabPaletteActivationResult =
| { status: 'failed'; reason: SimulatorTabPaletteActivationFailure }
export type SimulatorTabPaletteActivationTarget = {
executionHostId?: ExecutionHostId
tabId: string
worktreeId: string
}
export function activateSimulatorTabPaletteResult({
executionHostId,
tabId,
worktreeId
}: SimulatorTabPaletteActivationTarget): SimulatorTabPaletteActivationResult {
@@ -24,16 +27,15 @@ export function activateSimulatorTabPaletteResult({
return { status: 'failed', reason: 'missing-tab' }
}
// Why thread hostId: activateAndRevealWorktree resolves the worktree and stores
// activeWorkspaceExecutionHostId from it, so remote-hosted worktrees need it.
const worktree = initialState.getKnownWorktreeById(worktreeId)
const worktree = initialState.getKnownWorktreeById(worktreeId, executionHostId)
if (!worktree) {
return { status: 'failed', reason: 'missing-worktree' }
}
const targetHostId = executionHostId ?? worktree.hostId
const activated = activateAndRevealWorktree(
worktree.id,
worktree.hostId ? { executionHostId: worktree.hostId } : {}
targetHostId ? { executionHostId: targetHostId } : {}
)
if (!activated) {
return { status: 'failed', reason: 'missing-worktree' }
@@ -175,6 +175,17 @@ describe('activateWorkspaceTabPaletteResult', () => {
expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith('terminal-1')
})
it('scopes activation to the host carried by the search result', () => {
const executionHostId = 'runtime:host-1' as const
expect(activateWorkspaceTabPaletteResult({ ...makeResult(), executionHostId })).toEqual({
status: 'activated'
})
expect(mocks.store.getKnownWorktreeById).toHaveBeenCalledWith('wt-1', executionHostId)
expect(mocks.activateAndRevealWorktree).toHaveBeenCalledWith('wt-1', { executionHostId })
})
it('activates tabs in known folder or detected workspaces', () => {
mocks.store.worktreesByRepo = {}
mocks.store.getKnownWorktreeById.mockReturnValue({ id: 'wt-1', repoId: 'repo-1' })
@@ -6,6 +6,7 @@ import {
} from '@/runtime/web-runtime-session'
import { useAppStore } from '@/store'
import type { AppState } from '@/store/types'
import type { ExecutionHostId } from '../../../shared/execution-host'
import { activateAndRevealWorktree } from './worktree-activation'
import type { WorkspaceTabPaletteSearchResult } from './workspace-tab-palette-search'
@@ -23,7 +24,7 @@ export type WorkspaceTabPaletteActivationResult =
export type WorkspaceTabPaletteActivationTarget = Pick<
WorkspaceTabPaletteSearchResult,
'contentType' | 'entityId' | 'groupId' | 'tabId' | 'worktreeId'
>
> & { executionHostId?: ExecutionHostId }
type WorkspaceTabPaletteActivationState = Pick<
AppState,
@@ -42,7 +43,7 @@ function validateTarget(
state: WorkspaceTabPaletteActivationState,
result: WorkspaceTabPaletteActivationTarget
): WorkspaceTabPaletteActivationFailure | null {
if (!state.getKnownWorktreeById(result.worktreeId)) {
if (!state.getKnownWorktreeById(result.worktreeId, result.executionHostId)) {
return 'missing-worktree'
}
const group = (state.groupsByWorktree[result.worktreeId] ?? []).find(
@@ -82,7 +83,11 @@ export function activateWorkspaceTabPaletteResult(
return { status: 'failed', reason: initialFailure }
}
const activated = activateAndRevealWorktree(result.worktreeId)
const executionHostId =
result.executionHostId ?? initialState.getKnownWorktreeById(result.worktreeId)?.hostId
const activated = executionHostId
? activateAndRevealWorktree(result.worktreeId, { executionHostId })
: activateAndRevealWorktree(result.worktreeId)
if (!activated) {
return { status: 'failed', reason: 'missing-worktree' }
}