From 705b33f6d3d259090754bdd43de07d9841c134e9 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Fri, 7 Aug 2026 19:23:11 -0700 Subject: [PATCH] fix tab search host routing and churn --- .../TabBarCreateEntry.tab-results.test.tsx | 1 + .../tab-bar/open-tab-entry-dedupe.test.ts | 3 + .../tab-bar/open-tab-search-entries.ts | 81 +++++++++++++------ .../src/components/tab-bar/open-tab-search.ts | 32 +++++++- .../open-tab-selection-routing.test.ts | 13 ++- .../tab-bar/open-tab-selection-routing.ts | 3 + .../tab-bar/use-open-tab-search.test.ts | 35 ++++++++ .../components/tab-bar/use-open-tab-search.ts | 18 ++++- src/renderer/src/i18n/locales/es.json | 9 ++- src/renderer/src/i18n/locales/ja.json | 9 ++- src/renderer/src/i18n/locales/ko.json | 9 ++- src/renderer/src/i18n/locales/zh.json | 9 ++- .../lib/browser-page-palette-activation.ts | 8 +- .../lib/simulator-tab-palette-activation.ts | 10 ++- .../workspace-tab-palette-activation.test.ts | 11 +++ .../lib/workspace-tab-palette-activation.ts | 11 ++- 16 files changed, 215 insertions(+), 47 deletions(-) diff --git a/src/renderer/src/components/tab-bar/TabBarCreateEntry.tab-results.test.tsx b/src/renderer/src/components/tab-bar/TabBarCreateEntry.tab-results.test.tsx index 844882f9d04..bbf59640523 100644 --- a/src/renderer/src/components/tab-bar/TabBarCreateEntry.tab-results.test.tsx +++ b/src/renderer/src/components/tab-bar/TabBarCreateEntry.tab-results.test.tsx @@ -67,6 +67,7 @@ import TabBarCreateEntry from './TabBarCreateEntry' function terminalResult(overrides: Partial = {}): OpenTabSearchResult { return { + executionHostId: 'local', source: 'workspace', id: 'open-tab:workspace:tab-1', title: 'Add tab search and jump in worktree', diff --git a/src/renderer/src/components/tab-bar/open-tab-entry-dedupe.test.ts b/src/renderer/src/components/tab-bar/open-tab-entry-dedupe.test.ts index c98f2176ec4..053534c4d82 100644 --- a/src/renderer/src/components/tab-bar/open-tab-entry-dedupe.test.ts +++ b/src/renderer/src/components/tab-bar/open-tab-entry-dedupe.test.ts @@ -14,6 +14,7 @@ function editorTab( relativePath: string | null ): Extract { 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', diff --git a/src/renderer/src/components/tab-bar/open-tab-search-entries.ts b/src/renderer/src/components/tab-bar/open-tab-search-entries.ts index a57c6dfe891..25b6dffdccb 100644 --- a/src/renderer/src/components/tab-bar/open-tab-search-entries.ts +++ b/src/renderer/src/components/tab-bar/open-tab-search-entries.ts @@ -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 | null - worktree: Worktree | null + repo: Pick | 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, diff --git a/src/renderer/src/components/tab-bar/open-tab-search.ts b/src/renderer/src/components/tab-bar/open-tab-search.ts index 9e882984f2f..7fc414daf3b 100644 --- a/src/renderer/src/components/tab-bar/open-tab-search.ts +++ b/src/renderer/src/components/tab-bar/open-tab-search.ts @@ -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, diff --git a/src/renderer/src/components/tab-bar/open-tab-selection-routing.test.ts b/src/renderer/src/components/tab-bar/open-tab-selection-routing.test.ts index 330374f019a..17d03c1bdc0 100644 --- a/src/renderer/src/components/tab-bar/open-tab-selection-routing.test.ts +++ b/src/renderer/src/components/tab-bar/open-tab-selection-routing.test.ts @@ -30,7 +30,8 @@ vi.mock('@/components/browser-pane/browser-focus', () => ({ import { activateOpenTabSearchResult } from './open-tab-selection-routing' -const terminalResult: OpenTabSearchResult = { +const terminalResult: Extract = { + 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') }) diff --git a/src/renderer/src/components/tab-bar/open-tab-selection-routing.ts b/src/renderer/src/components/tab-bar/open-tab-selection-routing.ts index 3a024e52468..6c850bd00fc 100644 --- a/src/renderer/src/components/tab-bar/open-tab-selection-routing.ts +++ b/src/renderer/src/components/tab-bar/open-tab-selection-routing.ts @@ -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, diff --git a/src/renderer/src/components/tab-bar/use-open-tab-search.test.ts b/src/renderer/src/components/tab-bar/use-open-tab-search.test.ts index 62fdeb1c5e7..0182b57fe6d 100644 --- a/src/renderer/src/components/tab-bar/use-open-tab-search.test.ts +++ b/src/renderer/src/components/tab-bar/use-open-tab-search.test.ts @@ -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() diff --git a/src/renderer/src/components/tab-bar/use-open-tab-search.ts b/src/renderer/src/components/tab-bar/use-open-tab-search.ts index 0d0644c4725..9e4b660f847 100644 --- a/src/renderer/src/components/tab-bar/use-open-tab-search.ts +++ b/src/renderer/src/components/tab-bar/use-open-tab-search.ts @@ -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( diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index b776c361487..21bb7d9149a 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -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.", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 31f0d31a827..c2d28dcc7ed 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -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": "検索テキストが大きすぎます。", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 4e3c59f3569..edd4ce24e9e 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -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": "검색 텍스트가 너무 큽니다.", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 96a748a9898..0605c965926 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -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": "搜索文本过长。", diff --git a/src/renderer/src/lib/browser-page-palette-activation.ts b/src/renderer/src/lib/browser-page-palette-activation.ts index 896657fdd8a..935f6069aa5 100644 --- a/src/renderer/src/lib/browser-page-palette-activation.ts +++ b/src/renderer/src/lib/browser-page-palette-activation.ts @@ -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' } diff --git a/src/renderer/src/lib/simulator-tab-palette-activation.ts b/src/renderer/src/lib/simulator-tab-palette-activation.ts index 959bdd29c9d..abae54d775d 100644 --- a/src/renderer/src/lib/simulator-tab-palette-activation.ts +++ b/src/renderer/src/lib/simulator-tab-palette-activation.ts @@ -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' } diff --git a/src/renderer/src/lib/workspace-tab-palette-activation.test.ts b/src/renderer/src/lib/workspace-tab-palette-activation.test.ts index 5d0def8b8ba..386fb25d958 100644 --- a/src/renderer/src/lib/workspace-tab-palette-activation.test.ts +++ b/src/renderer/src/lib/workspace-tab-palette-activation.test.ts @@ -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' }) diff --git a/src/renderer/src/lib/workspace-tab-palette-activation.ts b/src/renderer/src/lib/workspace-tab-palette-activation.ts index 0b75ec1db81..688dae2fe7d 100644 --- a/src/renderer/src/lib/workspace-tab-palette-activation.ts +++ b/src/renderer/src/lib/workspace-tab-palette-activation.ts @@ -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' } }