mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 16:03:26 +00:00
fix(sidebar): preserve folder owner routing
This commit is contained in:
@@ -155,6 +155,20 @@ describe('filesystem-auth path containment', () => {
|
||||
expect(isPathAllowed(runtimePath, store)).toBe(false)
|
||||
})
|
||||
|
||||
it('does not authorize an SSH repo when stale host metadata says local', () => {
|
||||
const sshPath = resolve('/ssh-only/repo')
|
||||
const store = makeStore([
|
||||
{
|
||||
...repo,
|
||||
path: sshPath,
|
||||
connectionId: 'ssh-target',
|
||||
executionHostId: 'local'
|
||||
}
|
||||
])
|
||||
|
||||
expect(isPathAllowed(sshPath, store)).toBe(false)
|
||||
})
|
||||
|
||||
it('authorizes missing nested descendants under an allowed repo', async () => {
|
||||
const tempRoot = await mkdtemp(join(tmpdir(), 'orca-auth-missing-'))
|
||||
try {
|
||||
|
||||
@@ -68,7 +68,11 @@ export function invalidateAuthorizedRootsCache(): void {
|
||||
|
||||
function getLocalRepos(store: Store) {
|
||||
// Why: SSH repo paths are remote-host paths; treating them as local roots could authorize unrelated local folders or probe SSH-only paths.
|
||||
return store.getRepos().filter((repo) => getRepoExecutionHostId(repo) === LOCAL_EXECUTION_HOST_ID)
|
||||
return store
|
||||
.getRepos()
|
||||
.filter(
|
||||
(repo) => !repo.connectionId && getRepoExecutionHostId(repo) === LOCAL_EXECUTION_HOST_ID
|
||||
)
|
||||
}
|
||||
|
||||
type FolderAuthIndex = {
|
||||
|
||||
@@ -32,12 +32,7 @@ import {
|
||||
} from 'lucide-react'
|
||||
import { useAppStore } from '@/store'
|
||||
import type { AppState } from '@/store/types'
|
||||
import {
|
||||
selectRepoByIdForActiveWorkspace,
|
||||
useAllWorktrees,
|
||||
useRepoMap,
|
||||
useWorktreeMap
|
||||
} from '@/store/selectors'
|
||||
import { useAllWorktrees, useRepoMap, useWorktreeMap } from '@/store/selectors'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type {
|
||||
ProjectGroup,
|
||||
@@ -103,14 +98,14 @@ const DELETE_POSITION_RESTORE_STABLE_FRAMES = 6
|
||||
const PARENT_PICKER_EXIT_ANIMATION_MS = 200
|
||||
|
||||
export function findContextMenuRepo(
|
||||
repos: Repo[],
|
||||
repos: readonly Repo[] | undefined,
|
||||
repoId: string,
|
||||
executionHostId: ExecutionHostId
|
||||
): Repo | null {
|
||||
return selectRepoByIdForActiveWorkspace(
|
||||
{ repos, activeRepoId: repoId, activeWorkspaceExecutionHostId: executionHostId },
|
||||
repoId
|
||||
const matches = (repos ?? []).filter(
|
||||
(repo) => repo.id === repoId && getRepoExecutionHostId(repo) === executionHostId
|
||||
)
|
||||
return matches.length === 1 ? matches[0] : null
|
||||
}
|
||||
|
||||
export function resolveWorktreeContextMenuTargets(
|
||||
|
||||
@@ -99,8 +99,10 @@ import {
|
||||
getSidebarWorktreeSelectionId,
|
||||
getSingleProjectGroupMutationOwner,
|
||||
getGroupKeysForWorktree,
|
||||
getFolderWorkspaceRenderRowKey,
|
||||
getLineageGroupKey,
|
||||
getPinnedWorktreeDisplayPolicy,
|
||||
getSidebarWorkspaceActivationId,
|
||||
parseProjectGroupSidebarHeaderKey,
|
||||
type ProjectGroupMutationSelector,
|
||||
type PinnedWorktreeDisplayPolicy
|
||||
@@ -110,7 +112,7 @@ import {
|
||||
estimateRenderRowSize,
|
||||
extractWorktreeVirtualRowIndexes,
|
||||
getActiveStickyIndexesForScroll,
|
||||
getRenderRowKey,
|
||||
getRenderRowKey as getVirtualRenderRowKey,
|
||||
getStickyHeaderIndexes,
|
||||
getVirtualRowTransform,
|
||||
HOST_STICKY_GROUP_TOP_PX,
|
||||
@@ -324,7 +326,6 @@ import {
|
||||
getVisibleSidebarHostIdSet
|
||||
} from './worktree-list-host-filtering'
|
||||
import { getFolderWorkspaceCardPrDisplay } from './folder-workspace-card-pr-display'
|
||||
import { getRenderedWorktreesInSidebarOrder } from './worktree-sidebar-row-preference'
|
||||
import { getCyclableWorktreeIds, resolveCycledWorktreeId } from './worktree-keyboard-cycle'
|
||||
export {
|
||||
getScrollTopToRevealBounds,
|
||||
@@ -1240,10 +1241,6 @@ export function getPinnedWorktreeRevealCollapsedGroupKeys({
|
||||
return keys
|
||||
}
|
||||
|
||||
function uniqueWorktreeIds(ids: readonly string[]): string[] {
|
||||
return Array.from(new Set(ids))
|
||||
}
|
||||
|
||||
function getRenderedSidebarSelectionEntries(
|
||||
rows: readonly HostSectionRow[],
|
||||
defaultHostId: ExecutionHostId,
|
||||
@@ -1331,9 +1328,11 @@ function buildRenderableRows(rows: HostSectionRow[]): RenderRow[] {
|
||||
return renderRows
|
||||
}
|
||||
|
||||
// Why: getRenderRowKey lives with the other virtual-row helpers now; keep the
|
||||
// long-standing import path working for callers that reach for it here.
|
||||
export { getRenderRowKey }
|
||||
export function getRenderRowKey(row: RenderRow): string {
|
||||
return row.type === 'folder-workspace'
|
||||
? getFolderWorkspaceRenderRowKey(row)
|
||||
: getVirtualRenderRowKey(row)
|
||||
}
|
||||
|
||||
export function getWorktreeDragGroups(rows: HostSectionRow[]): WorktreeDragGroup[] {
|
||||
const groups: WorktreeDragGroup[] = []
|
||||
@@ -6106,24 +6105,28 @@ const WorktreeList = React.memo(function WorktreeList({
|
||||
const visibleHostResetKey = visibleWorkspaceHostIds?.join(',') ?? 'all'
|
||||
const viewportResetKey = `group:${groupBy}:host:${visibleHostResetKey}:lineage`
|
||||
|
||||
// Why: derive order from the built rows, not the flat worktrees array, so Cmd+1–9 match visual positions when grouping reorders cards.
|
||||
const renderedWorktrees = useMemo(
|
||||
() => getRenderedWorktreesInSidebarOrder(sectionRows, pinnedDisplayPolicy),
|
||||
[pinnedDisplayPolicy, sectionRows]
|
||||
)
|
||||
// Why: order-preserving sectionRows rebuilds must not give this array a new
|
||||
// identity — updateSelectionForGesture depends on it, and a fresh identity
|
||||
// there defeats React.memo bail-out for every WorktreeCard on epoch bumps.
|
||||
const renderedWorktreeIds = useReusedArrayIdentity(
|
||||
useMemo(
|
||||
() => uniqueWorktreeIds(renderedWorktrees.map((worktree) => worktree.id)),
|
||||
[renderedWorktrees]
|
||||
)
|
||||
)
|
||||
const renderedWorktreeSelectionEntries = useMemo(
|
||||
() => getRenderedSidebarSelectionEntries(sectionRows, defaultHostId, pinnedDisplayPolicy),
|
||||
[defaultHostId, pinnedDisplayPolicy, sectionRows]
|
||||
)
|
||||
// Why: shortcut activation needs the same owner-qualified folder identity as selection when same ids collide.
|
||||
const renderedWorktreeIds = useReusedArrayIdentity(
|
||||
useMemo(
|
||||
() =>
|
||||
Array.from(
|
||||
new Set(
|
||||
renderedWorktreeSelectionEntries.map(({ worktree }) =>
|
||||
getSidebarWorkspaceActivationId(
|
||||
worktree,
|
||||
getWorktreeExecutionHostId(worktree, undefined, defaultHostId),
|
||||
ownerQualifiedFolderWorkspaceIds
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
[defaultHostId, ownerQualifiedFolderWorkspaceIds, renderedWorktreeSelectionEntries]
|
||||
)
|
||||
)
|
||||
const renderedWorktreeSelectionIds = useReusedArrayIdentity(
|
||||
useMemo(
|
||||
() => renderedWorktreeSelectionEntries.map((entry) => entry.selectionId),
|
||||
|
||||
@@ -6,6 +6,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vite
|
||||
import type { ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { getFolderWorkspaceRowKey } from '../../../../shared/folder-workspaces'
|
||||
import { folderWorkspaceToWorktree } from '../../../../shared/folder-workspace-worktree'
|
||||
import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import type { FolderWorkspace, ProjectGroup, Repo, Worktree } from '../../../../shared/types'
|
||||
import {
|
||||
buildRows,
|
||||
@@ -14,16 +15,27 @@ import {
|
||||
findProjectGroupForRepo,
|
||||
findProjectGroupForSidebarOwner,
|
||||
getAmbiguousFolderWorkspaceSidebarIds,
|
||||
getFolderWorkspaceRenderRowKey,
|
||||
getProjectGroupMutationSelector,
|
||||
getProjectGroupHeaderKey,
|
||||
getProjectGroupSidebarIdentity,
|
||||
getSidebarWorkspaceActivationId,
|
||||
getSidebarWorktreeSelectionId,
|
||||
getSingleProjectGroupMutationOwner,
|
||||
hasSingleProjectGroupMutationOwner,
|
||||
parseProjectGroupSidebarHeaderKey
|
||||
} from './worktree-list-groups'
|
||||
import { addHostSectionRows } from './host-section-rows'
|
||||
import { getRenderRowKey } from './worktree-list-virtual-rows'
|
||||
import {
|
||||
getRenderRowKey as getVirtualRenderRowKey,
|
||||
type RenderRow
|
||||
} from './worktree-list-virtual-rows'
|
||||
|
||||
function getRenderRowKey(row: RenderRow): string {
|
||||
return row.type === 'folder-workspace'
|
||||
? getFolderWorkspaceRenderRowKey(row)
|
||||
: getVirtualRenderRowKey(row)
|
||||
}
|
||||
|
||||
function sidebarIdentityGroup(id: string, executionHostId: string): ProjectGroup {
|
||||
return {
|
||||
@@ -228,6 +240,12 @@ describe('project-group sidebar identity', () => {
|
||||
expect(getSidebarWorktreeSelectionId(local, undefined, 'local')).not.toBe(
|
||||
getSidebarWorktreeSelectionId(runtime, undefined, 'local')
|
||||
)
|
||||
expect(getSidebarWorkspaceActivationId(local, 'local', new Set(['same-id']))).toBe(
|
||||
folderWorkspaceKey('same-id', 'local')
|
||||
)
|
||||
expect(getSidebarWorkspaceActivationId(runtime, 'runtime:env-1', new Set(['same-id']))).toBe(
|
||||
folderWorkspaceKey('same-id', 'runtime:env-1')
|
||||
)
|
||||
})
|
||||
|
||||
it('does not fall back to a foreign group for an explicit stale owner', () => {
|
||||
@@ -507,7 +525,7 @@ describe('duplicate project-group header identity (#12532)', () => {
|
||||
|
||||
const folderRow = rows.find((row) => row.type === 'folder-workspace')
|
||||
expect(folderRow?.key).toBe('folder-workspace:@owner:runtime%3Aenv-1:same-folder')
|
||||
expect(folderRow && getRenderRowKey(folderRow)).toBe(
|
||||
expect(folderRow && getFolderWorkspaceRenderRowKey(folderRow)).toBe(
|
||||
'folder-workspace:@owner:runtime%3Aenv-1:same-folder'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import type { Worktree } from '../../../../shared/types'
|
||||
import type { FolderWorkspace, ProjectGroup, Worktree } from '../../../../shared/types'
|
||||
import type { AppState } from '@/store/types'
|
||||
import { getHostDisplayLabelOverrides } from '../../../../shared/host-setting-overrides'
|
||||
import {
|
||||
ALL_EXECUTION_HOSTS_SCOPE,
|
||||
getSettingsFocusedExecutionHostId
|
||||
getSettingsFocusedExecutionHostId,
|
||||
type ExecutionHostId
|
||||
} from '../../../../shared/execution-host'
|
||||
import { getRepoMapFromState, getWorktreeMapFromState } from '@/store/selectors'
|
||||
import { getProjectHostSetupProjectionFromState } from '@/store/project-host-setup-selector'
|
||||
import { buildRows, getPinnedWorktreeDisplayPolicy } from './worktree-list-groups'
|
||||
import {
|
||||
buildProjectGroupSidebarIndex,
|
||||
buildRows,
|
||||
getAmbiguousFolderWorkspaceSidebarIds,
|
||||
getPinnedWorktreeDisplayPolicy,
|
||||
getProjectGroupMutationSelector,
|
||||
getSidebarWorkspaceActivationId,
|
||||
type WorktreeRow
|
||||
} from './worktree-list-groups'
|
||||
import { addHostSectionRows } from './host-section-rows'
|
||||
import { orderHostSectionOptions } from './host-section-order'
|
||||
import { buildSidebarHostOptions } from './sidebar-host-options'
|
||||
import { getLogicalRepoOrderRankById } from './project-header-drop'
|
||||
import { getRenderedWorktreesInSidebarOrder } from './worktree-sidebar-row-preference'
|
||||
import { getPreferredWorktreeRows } from './worktree-sidebar-row-preference'
|
||||
import { folderWorkspaceToWorktree } from '../../../../shared/folder-workspace-worktree'
|
||||
import { resolveFolderWorkspaceCatalogOwnerHostId } from '../../../../shared/folder-workspaces'
|
||||
import { selectWorktreeListReviewCacheInputs } from './worktree-list-review-cache-inputs'
|
||||
import {
|
||||
filterFolderWorkspacesForVisibleHosts,
|
||||
@@ -25,6 +36,16 @@ const EMPTY_IMPORTED_BY_REPO = Object.freeze(new Map()) as never
|
||||
const EMPTY_INBOX_BY_REPO = Object.freeze(new Map()) as never
|
||||
const EMPTY_PENDING_CREATIONS = Object.freeze([]) as never
|
||||
|
||||
function getFolderOwnerHostId(row: {
|
||||
folderWorkspace: FolderWorkspace
|
||||
projectGroup: ProjectGroup
|
||||
}): ExecutionHostId {
|
||||
return (
|
||||
resolveFolderWorkspaceCatalogOwnerHostId(row.folderWorkspace, [row.projectGroup]) ??
|
||||
getProjectGroupMutationSelector(row.projectGroup).ownerHostId
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Orders already-filtered worktrees the way the sidebar would render them, for
|
||||
* Cmd+1–9 numbering while WorktreeList is unmounted (#9497).
|
||||
@@ -112,11 +133,28 @@ export function computeRenderedSidebarWorktreeOrder(
|
||||
})
|
||||
: rows
|
||||
|
||||
return Array.from(
|
||||
new Set(
|
||||
getRenderedWorktreesInSidebarOrder(sectionRows, pinnedDisplayPolicy).map(
|
||||
(worktree) => worktree.id
|
||||
)
|
||||
)
|
||||
const itemRows = sectionRows.filter((row): row is WorktreeRow => row.type === 'item')
|
||||
const preferredItemRowKeys = new Set(
|
||||
getPreferredWorktreeRows(itemRows, pinnedDisplayPolicy).map((row) => row.rowKey)
|
||||
)
|
||||
const ambiguousFolderWorkspaceIds = getAmbiguousFolderWorkspaceSidebarIds(
|
||||
buildProjectGroupSidebarIndex(projectGroups),
|
||||
state.folderWorkspaces
|
||||
)
|
||||
|
||||
const workspaceIds: string[] = []
|
||||
for (const row of sectionRows) {
|
||||
if (row.type === 'item' && preferredItemRowKeys.has(row.rowKey)) {
|
||||
workspaceIds.push(row.worktree.id)
|
||||
} else if (row.type === 'folder-workspace') {
|
||||
workspaceIds.push(
|
||||
getSidebarWorkspaceActivationId(
|
||||
folderWorkspaceToWorktree(row.folderWorkspace),
|
||||
getFolderOwnerHostId(row),
|
||||
ambiguousFolderWorkspaceIds
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return Array.from(new Set(workspaceIds))
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ import {
|
||||
UNGROUPED_PROJECT_GROUP_KEY
|
||||
} from '../../../../shared/project-groups'
|
||||
import {
|
||||
folderWorkspaceKey,
|
||||
getProjectGroupSelectorKey,
|
||||
parseWorkspaceKey,
|
||||
parseProjectGroupSelectorKey
|
||||
} from '../../../../shared/workspace-scope'
|
||||
import {
|
||||
@@ -78,6 +80,17 @@ export function getSidebarWorktreeSelectionId(
|
||||
return JSON.stringify([getWorktreeExecutionHostId(worktree, repo, defaultHostId), worktree.id])
|
||||
}
|
||||
|
||||
export function getSidebarWorkspaceActivationId(
|
||||
worktree: Worktree,
|
||||
ownerHostId: ExecutionHostId,
|
||||
ambiguousFolderWorkspaceIds: ReadonlySet<string>
|
||||
): string {
|
||||
const scope = parseWorkspaceKey(worktree.id)
|
||||
return scope?.type === 'folder' && ambiguousFolderWorkspaceIds.has(scope.folderWorkspaceId)
|
||||
? folderWorkspaceKey(scope.folderWorkspaceId, ownerHostId)
|
||||
: worktree.id
|
||||
}
|
||||
|
||||
export type ProjectGroupSidebarIdentity = string
|
||||
|
||||
export type ProjectGroupMutationSelector = {
|
||||
@@ -375,6 +388,10 @@ export type FolderWorkspaceRow = {
|
||||
groupDepth: number
|
||||
}
|
||||
|
||||
export function getFolderWorkspaceRenderRowKey(row: Pick<FolderWorkspaceRow, 'key'>): string {
|
||||
return row.key
|
||||
}
|
||||
|
||||
/** Minimal shape buildRows needs for an in-flight create. Deliberately not the
|
||||
* full PendingWorktreeCreation: row identity depends only on which creates
|
||||
* exist and their repo, so callers can subscribe on this stable shape and keep
|
||||
|
||||
@@ -38,7 +38,7 @@ export function getRenderRowKey(row: RenderRow): string {
|
||||
return `pending:${row.creationId}`
|
||||
}
|
||||
if (row.type === 'folder-workspace') {
|
||||
return row.key
|
||||
return `folder-workspace:${row.folderWorkspace.id}`
|
||||
}
|
||||
return `wt:${row.rowKey}`
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import {
|
||||
buildProjectGroupOwnerIndex,
|
||||
getProjectGroupIdentity,
|
||||
getProjectGroupOwnerHostId,
|
||||
getProjectGroupOwnerSubtreeIdentities
|
||||
getProjectGroupOwnerSubtreeIdentities,
|
||||
getProjectGroupSubtreeIds
|
||||
} from '../../../shared/project-groups'
|
||||
import {
|
||||
resolveFolderWorkspaceCatalogOwnerHostId,
|
||||
@@ -29,6 +30,7 @@ function getFolderScopeCandidateRepos(args: {
|
||||
folderPath: string
|
||||
projectGroup: ProjectGroup
|
||||
ownerHostId: ExecutionHostId
|
||||
inferLegacyOwner: boolean
|
||||
connectionId?: string | null
|
||||
projectGroups: readonly ProjectGroup[]
|
||||
repos: readonly Repo[]
|
||||
@@ -37,22 +39,21 @@ function getFolderScopeCandidateRepos(args: {
|
||||
args.projectGroups,
|
||||
args.projectGroup
|
||||
)
|
||||
const groupRepos = args.repos.filter(
|
||||
(repo) =>
|
||||
typeof repo.projectGroupId === 'string' &&
|
||||
groupIdentities.has(
|
||||
getProjectGroupIdentity(repo.projectGroupId, getRepoExecutionHostId(repo))
|
||||
)
|
||||
)
|
||||
const legacyGroupIds = args.inferLegacyOwner
|
||||
? getProjectGroupSubtreeIds(args.projectGroups, args.projectGroup.id)
|
||||
: null
|
||||
const repoIsInGroup = (repo: Repo): boolean =>
|
||||
typeof repo.projectGroupId === 'string' &&
|
||||
(legacyGroupIds
|
||||
? legacyGroupIds.has(repo.projectGroupId)
|
||||
: groupIdentities.has(
|
||||
getProjectGroupIdentity(repo.projectGroupId, getRepoExecutionHostId(repo))
|
||||
))
|
||||
const groupRepos = args.repos.filter((repo) => repoIsInGroup(repo))
|
||||
const pathRepos = args.repos.filter(
|
||||
(repo) =>
|
||||
getRepoExecutionHostId(repo) === args.ownerHostId &&
|
||||
!(
|
||||
typeof repo.projectGroupId === 'string' &&
|
||||
groupIdentities.has(
|
||||
getProjectGroupIdentity(repo.projectGroupId, getRepoExecutionHostId(repo))
|
||||
)
|
||||
) &&
|
||||
(args.inferLegacyOwner || getRepoExecutionHostId(repo) === args.ownerHostId) &&
|
||||
!repoIsInGroup(repo) &&
|
||||
isPathInsideOrEqual(args.folderPath, repo.path)
|
||||
)
|
||||
if (args.connectionId) {
|
||||
@@ -75,14 +76,20 @@ function findFolderWorkspaceScope(
|
||||
state: FolderWorkspaceConnectionState,
|
||||
folderWorkspaceId: string,
|
||||
ownerHostId?: ExecutionHostId
|
||||
): { workspace: FolderWorkspace; projectGroup: ProjectGroup; ownerHostId: ExecutionHostId } | null {
|
||||
): {
|
||||
workspace: FolderWorkspace
|
||||
projectGroup: ProjectGroup
|
||||
ownerHostId: ExecutionHostId
|
||||
inferLegacyOwner: boolean
|
||||
} | null {
|
||||
const candidates = state.folderWorkspaces.filter((entry) => entry.id === folderWorkspaceId)
|
||||
const activeScope = parseWorkspaceKey(state.activeWorktreeId ?? '')
|
||||
const activeOwnerHostId =
|
||||
activeScope?.type === 'folder' && activeScope.folderWorkspaceId === folderWorkspaceId
|
||||
? (activeScope.ownerHostId ?? state.activeWorkspaceExecutionHostId)
|
||||
? (activeScope.ownerHostId ??
|
||||
(candidates.length > 1 ? state.activeWorkspaceExecutionHostId : undefined))
|
||||
: undefined
|
||||
const requestedOwnerHostId = ownerHostId ?? activeOwnerHostId ?? undefined
|
||||
const candidates = state.folderWorkspaces.filter((entry) => entry.id === folderWorkspaceId)
|
||||
const workspace = requestedOwnerHostId
|
||||
? candidates.find(
|
||||
(entry) =>
|
||||
@@ -95,17 +102,16 @@ function findFolderWorkspaceScope(
|
||||
if (!workspace) {
|
||||
return null
|
||||
}
|
||||
const projectGroup = resolveFolderWorkspaceProjectGroupWithLegacySsh(
|
||||
buildProjectGroupOwnerIndex(state.projectGroups),
|
||||
workspace
|
||||
)
|
||||
const projectGroupIndex = buildProjectGroupOwnerIndex(state.projectGroups)
|
||||
const projectGroup = resolveFolderWorkspaceProjectGroupWithLegacySsh(projectGroupIndex, workspace)
|
||||
if (!projectGroup) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
workspace,
|
||||
projectGroup,
|
||||
ownerHostId: getProjectGroupOwnerHostId(projectGroup)
|
||||
ownerHostId: getProjectGroupOwnerHostId(projectGroup),
|
||||
inferLegacyOwner: projectGroupIndex.byId.get(projectGroup.id)?.length === 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +128,7 @@ export function getFolderWorkspaceCandidateRepos(
|
||||
folderPath: scope.workspace.folderPath,
|
||||
projectGroup: scope.projectGroup,
|
||||
ownerHostId: scope.ownerHostId,
|
||||
inferLegacyOwner: scope.inferLegacyOwner,
|
||||
connectionId: scope.workspace.connectionId ?? scope.projectGroup.connectionId ?? null,
|
||||
projectGroups: state.projectGroups,
|
||||
repos: state.repos
|
||||
|
||||
@@ -237,12 +237,16 @@ export function activateAndRevealFolderWorkspace(
|
||||
opts && 'runtimeEnvironmentId' in opts
|
||||
? (opts.runtimeEnvironmentId ?? null)
|
||||
: getRuntimeEnvironmentIdForFolderWorkspace(state, folderWorkspaceId, ownerHostId)
|
||||
const pathStatusOwnerHostId =
|
||||
state.folderWorkspaces.filter((workspace) => workspace.id === folderWorkspaceId).length > 1
|
||||
? ownerHostId
|
||||
: undefined
|
||||
const pathStatus = state.getFreshFolderWorkspacePathStatus(
|
||||
{
|
||||
scope: 'folder-workspace',
|
||||
folderWorkspaceId
|
||||
},
|
||||
{ runtimeEnvironmentId, ownerHostId }
|
||||
{ runtimeEnvironmentId, ownerHostId: pathStatusOwnerHostId }
|
||||
)
|
||||
if (folderWorkspaceActivationBlocked(pathStatus)) {
|
||||
const title =
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { FolderWorkspace, ProjectGroup, Repo } from '../../../../shared/typ
|
||||
import { getFolderWorkspaceRowKey } from '../../../../shared/folder-workspaces'
|
||||
import { folderWorkspaceKey, parseWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import { buildHostIdByWorktreeId } from '../../lib/workspace-session-host-persistence'
|
||||
import { computeRenderedSidebarWorktreeOrder } from '../../components/sidebar/rendered-sidebar-worktree-order'
|
||||
import {
|
||||
getFolderWorkspaceCandidateRepos,
|
||||
getFolderWorkspaceConnectionId
|
||||
@@ -51,6 +52,25 @@ describe('same-id cross-host folder workspace identity', () => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('keeps both owner-qualified folders in closed-sidebar shortcut order', () => {
|
||||
const store = createTestStore()
|
||||
store.setState({
|
||||
projectGroups: [
|
||||
makeGroup({ executionHostId: 'local', parentPath: '/local' }),
|
||||
makeGroup({ executionHostId: 'runtime:env-1', parentPath: '/runtime' })
|
||||
],
|
||||
folderWorkspaces: [
|
||||
makeFolder({ executionHostId: 'local', folderPath: '/local/folder' }),
|
||||
makeFolder({ executionHostId: 'runtime:env-1', folderPath: '/runtime/folder' })
|
||||
]
|
||||
})
|
||||
|
||||
expect(computeRenderedSidebarWorktreeOrder(store.getState(), [])).toEqual([
|
||||
folderWorkspaceKey('same-id', 'local'),
|
||||
folderWorkspaceKey('same-id', 'runtime:env-1')
|
||||
])
|
||||
})
|
||||
|
||||
it('deletes only the owner-qualified folder row and its session keys', async () => {
|
||||
const local = makeFolder({
|
||||
name: 'Local',
|
||||
|
||||
@@ -2932,7 +2932,7 @@ export const createRepoSlice: StateCreator<AppState, [], [], RepoSlice> = (set,
|
||||
const target = getActiveRuntimeTarget({ activeRuntimeEnvironmentId: runtimeEnvironmentId })
|
||||
const updateIdentity = getFolderWorkspaceUpdateIdentity(ownerHostId, folderWorkspaceId)
|
||||
const includeOwnerHostId =
|
||||
executionHostId !== undefined ||
|
||||
options?.executionHostId !== undefined ||
|
||||
state.folderWorkspaces.filter((workspace) => workspace.id === folderWorkspaceId).length > 1
|
||||
// Why: same gate as folderWorkspace.create — an older paired runtime would drop the Jira link silently.
|
||||
if (
|
||||
|
||||
@@ -6324,7 +6324,7 @@ export const createWorktreeSlice: StateCreator<AppState, [], [], WorktreeSlice>
|
||||
void get().updateFolderWorkspace(
|
||||
folderWorkspaceId,
|
||||
{ isUnread: false },
|
||||
{ executionHostId: ownerHostId }
|
||||
folderWorkspaceMutationOptions(get().folderWorkspaces, folderWorkspaceId, ownerHostId)
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -96,7 +96,12 @@ export function resolveFolderWorkspaceCatalogOwnerHostIdFromIndex(
|
||||
if (group) {
|
||||
return getFolderWorkspaceProjectGroupOwnerHostId(workspace, index)
|
||||
}
|
||||
return workspace.connectionId ? toSshExecutionHostId(workspace.connectionId) : null
|
||||
if (workspace.connectionId !== undefined) {
|
||||
return workspace.connectionId
|
||||
? toSshExecutionHostId(workspace.connectionId)
|
||||
: LOCAL_EXECUTION_HOST_ID
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function getFolderWorkspaceOwnerIdentity(
|
||||
|
||||
Reference in New Issue
Block a user