fix(automations): find folder-workspace targets on the Automations page (#27416)

Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
OrcaWin
2026-10-11 01:32:55 -07:00
committed by GitHub
co-authored by m4air
parent c5778f3567
commit 264dc343d6
5 changed files with 102 additions and 21 deletions
@@ -25,7 +25,12 @@ import {
matchAutomationListSearchRowKeys
} from './automation-list-search-rows'
import { REPO_ID } from './automations-page-fixtures'
import { automationRepoForRow, automationWorktreeForRow } from './automation-list-row-identity'
import { makeFolderWorkspace } from '../sidebar/folder-workspace-composer-submit-fixtures'
import {
automationFolderWorktreeForRow,
automationRepoForRow,
automationWorktreeForRow
} from './automation-list-row-identity'
const repoMap = new Map([
[REPO_ID, { id: REPO_ID, displayName: 'orca', path: '/src/orca' } as Repo]
@@ -177,4 +182,17 @@ describe('automation list row identity across hosts', () => {
})
).toEqual(['Desktop workspace', 'Runtime workspace'])
})
it('resolves a folder workspace target inside each row authority', () => {
const rows = collidedListRows().rows
const folders = [
makeFolderWorkspace({ id: 'fw-1', name: 'Desktop folder' }),
makeFolderWorkspace({ id: 'fw-1', name: 'Runtime folder', executionHostId: 'runtime:gpu' })
]
expect(
rows.map((row) => automationFolderWorktreeForRow(row, folders, 'folder:fw-1')?.displayName)
).toEqual(['Desktop folder', 'Runtime folder'])
expect(automationFolderWorktreeForRow(rows[0], folders, 'ws-1')).toBeUndefined()
})
})
@@ -19,11 +19,15 @@ import type { AutomationUsageSummary } from '../../../../shared/automation-usage
import { getAutomationRunRepoId } from '../../../../shared/automation-run-identity'
import type { StableAutomationCatalogRef } from '../../../../shared/automation-owner-ref'
import {
getConnectionExecutionHostId,
getRepoExecutionHostId,
getWorktreeExecutionHostId
} from '../../../../shared/execution-host'
import type { FolderWorkspace } from '../../../../shared/folder-workspace-types'
import type { Repo } from '../../../../shared/repo-types'
import type { Worktree } from '../../../../shared/worktree/types'
import { parseWorkspaceKey } from '../../../../shared/workspace-scope'
import { getFolderWorkspaceWorktree } from '../../store/slices/worktrees/listing/detected-worktree-meta'
export type AutomationListRow = {
/** Authority-qualified and incarnation-free; see the module note. */
@@ -80,15 +84,41 @@ export function automationRepoForRow(
if (!authority) {
return fallback.get(repoId)
}
return repos.find((repo) => {
const host = getRepoExecutionHostId(repo)
return (
repo.id === repoId &&
(authority.kind === 'runtime'
? host === `runtime:${encodeURIComponent(authority.environmentId)}`
: !host.startsWith('runtime:'))
)
})
return repos.find(
(repo) => repo.id === repoId && authorityOwnsHost(authority, getRepoExecutionHostId(repo))
)
}
function authorityOwnsHost(
authority: StableAutomationCatalogRef['authority'],
hostId: string
): boolean {
return authority.kind === 'runtime'
? hostId === `runtime:${encodeURIComponent(authority.environmentId)}`
: !hostId.startsWith('runtime:')
}
/** Folder workspaces are not in `worktreesByRepo`; dispatch resolves `folder:<id>` the same way. */
export function automationFolderWorktreeForRow(
row: AutomationListRow,
folderWorkspaces: readonly FolderWorkspace[],
workspaceId: string | null | undefined = row.automation.workspaceId
): Worktree | undefined {
const scope = workspaceId ? parseWorkspaceKey(workspaceId) : null
if (scope?.type !== 'folder') {
return undefined
}
const authority = row.catalogRef?.authority
const matches = folderWorkspaces.filter(
(workspace) =>
workspace.id === scope.folderWorkspaceId &&
(!authority ||
authorityOwnsHost(
authority,
workspace.executionHostId ?? getConnectionExecutionHostId(workspace.connectionId)
))
)
return matches.length === 1 ? getFolderWorkspaceWorktree(matches[0]) : undefined
}
export function automationWorktreeForRow(
@@ -0,0 +1,28 @@
// @vitest-environment happy-dom
import { renderHook } from '@testing-library/react'
import { afterEach, describe, expect, it } from 'vitest'
import { useAppStore } from '@/store'
import { makeFolderWorkspace } from '../sidebar/folder-workspace-composer-submit-fixtures'
import { makeAutomation } from './automations-page-fixtures'
import { unscopedAutomationListRows } from './automation-list-row-identity'
import { useAutomationsPageStoreState } from './use-automations-page-store-state'
const initialState = useAppStore.getState()
afterEach(() => {
useAppStore.setState(initialState, true)
})
describe('automation rows that target a folder workspace', () => {
it('find the folder workspace instead of reporting it gone', () => {
useAppStore.setState({
folderWorkspaces: [makeFolderWorkspace({ id: 'fw-1', name: 'App' })]
})
const [row] = unscopedAutomationListRows([
makeAutomation({ workspaceMode: 'existing', workspaceId: 'folder:fw-1' })
])
const { result } = renderHook(() => useAutomationsPageStoreState())
expect(result.current.worktreeForRow(row, undefined)?.displayName).toBe('App')
})
})
@@ -6,6 +6,7 @@ import { getAgentCatalog } from '@/lib/agent-catalog'
import { useAppStore } from '@/store'
import { useRepoMap, useWorktreeMap } from '@/store/selectors'
import {
automationFolderWorktreeForRow,
automationRepoForRow,
automationWorktreeForRow,
type AutomationListRow
@@ -18,6 +19,7 @@ export function useAutomationsPageStoreState() {
const repos = useAppStore((s) => s.repos)
const projectHostSetups = useAppStore((s) => s.projectHostSetups)
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
const folderWorkspaces = useAppStore((s) => s.folderWorkspaces)
const unifiedTabsByWorktree = useAppStore((s) => s.unifiedTabsByWorktree)
const terminalLayoutsByTabId = useAppStore((s) => s.terminalLayoutsByTabId)
const ptyIdsByTabId = useAppStore((s) => s.ptyIdsByTabId)
@@ -52,8 +54,9 @@ export function useAutomationsPageStoreState() {
repo: Repo | undefined,
workspaceId: string | null | undefined = row.automation.workspaceId
): Worktree | undefined =>
automationFolderWorktreeForRow(row, folderWorkspaces, workspaceId) ??
automationWorktreeForRow(row, worktreesByRepo, repo, worktreeMap, workspaceId),
[worktreeMap, worktreesByRepo]
[folderWorkspaces, worktreeMap, worktreesByRepo]
)
const enabledAgents = filterEnabledTuiAgents(AGENTS, settings?.disabledTuiAgents)
const defaultAgent =
@@ -78,6 +78,17 @@ export function folderWorkspaceMatchesHost(
)
}
/** Stable synthetic row per folder workspace object, so selectors keep referential equality. */
export function getFolderWorkspaceWorktree(folderWorkspace: FolderWorkspace): Worktree {
const cached = folderWorkspaceWorktreeCache.get(folderWorkspace)
if (cached) {
return cached
}
const worktree = folderWorkspaceToWorktree(folderWorkspace)
folderWorkspaceWorktreeCache.set(folderWorkspace, worktree)
return worktree
}
export function findKnownWorktreeById(
state: Pick<
AppState,
@@ -105,16 +116,7 @@ export function findKnownWorktreeById(
workspace.id === workspaceScope.folderWorkspaceId &&
(!executionHostId || folderWorkspaceMatchesHost(workspace, executionHostId))
)
if (!folderWorkspace) {
return undefined
}
const cached = folderWorkspaceWorktreeCache.get(folderWorkspace)
if (cached) {
return cached
}
const worktree = folderWorkspaceToWorktree(folderWorkspace)
folderWorkspaceWorktreeCache.set(folderWorkspace, worktree)
return worktree
return folderWorkspace ? getFolderWorkspaceWorktree(folderWorkspace) : undefined
}
const visible = executionHostId
? (findIndexedWorktreeOwnerForHost(