mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 00:03:46 +00:00
fix tab search host routing and churn
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "検索テキストが大きすぎます。",
|
||||
|
||||
@@ -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": "검색 텍스트가 너무 큽니다.",
|
||||
|
||||
@@ -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' }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user