mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 00:03:08 +00:00
Fix folder workspace status icons
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -21,6 +21,7 @@ let tabsByWorktree: Record<string, { id: string }[]> = {}
|
||||
let ptyIdsByTabId: Record<string, string[]> = {}
|
||||
let browserTabsByWorktree: Record<string, { id: string }[]> = {}
|
||||
let settings: Partial<GlobalSettings> | null = null
|
||||
let projectGroups: unknown[] = []
|
||||
let workspaceDeleteModifierPressed = false
|
||||
let gitConflictOperationByWorktree: Record<string, GitConflictOperation> = {}
|
||||
let WorktreeCard: typeof WorktreeCardComponent
|
||||
@@ -35,7 +36,7 @@ vi.mock('@/store', () => ({
|
||||
hostedReviewCache: {},
|
||||
issueCache: {},
|
||||
openModal,
|
||||
projectGroups: [],
|
||||
projectGroups,
|
||||
remoteBranchConflictByWorktreeId: {},
|
||||
settings,
|
||||
sshConnectionStates: new Map(),
|
||||
@@ -144,6 +145,7 @@ describe('WorktreeCard quick actions', () => {
|
||||
ptyIdsByTabId = {}
|
||||
browserTabsByWorktree = {}
|
||||
settings = null
|
||||
projectGroups = []
|
||||
workspaceDeleteModifierPressed = false
|
||||
gitConflictOperationByWorktree = {}
|
||||
})
|
||||
@@ -217,6 +219,28 @@ describe('WorktreeCard quick actions', () => {
|
||||
expect(markup).toContain('data-worktree-card-meta-row=""')
|
||||
})
|
||||
|
||||
it('does not render a branch icon for synthetic folder workspace path identity', () => {
|
||||
settings = { compactWorktreeCards: true, experimentalNewWorktreeCardStyle: true }
|
||||
worktreeCardProperties = ['status', 'branch']
|
||||
projectGroups = [{ id: 'project-group-1' }]
|
||||
|
||||
const markup = renderToStaticMarkup(
|
||||
<WorktreeCard
|
||||
worktree={makeWorktree({
|
||||
id: 'folder:folder-1',
|
||||
displayName: 'Docs folder',
|
||||
branch: '',
|
||||
path: '/repo/worktrees/quick-action'
|
||||
})}
|
||||
repo={undefined}
|
||||
isActive={false}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(markup).toContain('/repo/worktrees/quick-action')
|
||||
expect(markup).not.toContain('lucide-git-branch')
|
||||
})
|
||||
|
||||
it('does not render a pending first-agent rename title badge', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<WorktreeCard
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
import { WorktreeCardPortsDetails, WorktreeCardPortsTrigger } from './WorktreeCardPorts'
|
||||
import { writeWorkspaceDragData } from './workspace-status'
|
||||
import { getWorktreeCardPrDisplay } from './worktree-card-pr-display'
|
||||
import type { WorktreeCardPrDisplay } from './worktree-card-pr-display'
|
||||
import {
|
||||
coerceWorktreeCardVisibleTitle,
|
||||
getWorktreeCardTitleDisplay
|
||||
@@ -121,6 +122,7 @@ type WorktreeCardProps = {
|
||||
onCardDragEnd?: (event: React.DragEvent<HTMLDivElement>) => void
|
||||
nativeDragEnabled?: boolean
|
||||
affiliateListMode?: boolean
|
||||
statusPrDisplay?: WorktreeCardPrDisplay | null
|
||||
}
|
||||
|
||||
const EMPTY_WORKSPACE_PORTS = []
|
||||
@@ -212,7 +214,8 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
lineageChildrenStyle,
|
||||
onLineageToggle,
|
||||
isLineageDropTarget = false,
|
||||
affiliateListMode = false
|
||||
affiliateListMode = false,
|
||||
statusPrDisplay = null
|
||||
}: WorktreeCardProps) {
|
||||
const openModal = useAppStore((s) => s.openModal)
|
||||
const openTaskPage = useAppStore((s) => s.openTaskPage)
|
||||
@@ -380,12 +383,6 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
const folderMetaRowContent = newCardStyle
|
||||
? hasPathIdentityEnabled && Boolean(folderPathIdentityDisplay)
|
||||
: isFolder
|
||||
const statusIdentityLabel = hasProjectGroups
|
||||
? translate(
|
||||
'auto.components.sidebar.WorktreeCard.branchFolderPathIdentity',
|
||||
'Branch or folder path'
|
||||
)
|
||||
: translate('auto.components.sidebar.WorktreeCard.branchIdentity', 'Branch')
|
||||
const hostedReviewCacheKey =
|
||||
repo && branch
|
||||
? getHostedReviewCacheKey(
|
||||
@@ -955,6 +952,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
const hoverIssue = issueDisplay
|
||||
const hoverLinearIssue = linearIssueDisplay
|
||||
const hoverReview = prDisplay
|
||||
const statusLaneReview = statusPrDisplay ?? hoverReview
|
||||
const hoverComment = worktree.comment
|
||||
const metaIssue = showIssue ? hoverIssue : null
|
||||
const metaLinearIssue = showLinearIssue ? hoverLinearIssue : null
|
||||
@@ -1275,10 +1273,9 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
unreadTooltip={unreadTooltip}
|
||||
onPointerDown={stopQuickActionPointerPropagation}
|
||||
onToggleUnread={handleToggleUnreadQuick}
|
||||
prDisplay={hoverReview}
|
||||
prDisplay={statusLaneReview}
|
||||
newCardStyle={newCardStyle}
|
||||
hasBranchIdentity={Boolean(identityDisplay)}
|
||||
branchIdentityLabel={statusIdentityLabel}
|
||||
hasBranchIdentity={Boolean(branchIdentityDisplay)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -57,6 +57,7 @@ import type {
|
||||
ProjectOrderBy,
|
||||
WorktreeLineage,
|
||||
WorktreeMeta,
|
||||
WorkspaceLineage,
|
||||
WorkspaceStatus,
|
||||
WorkspaceStatusDefinition
|
||||
} from '../../../../shared/types'
|
||||
@@ -255,6 +256,7 @@ import {
|
||||
getFolderWorkspaceExecutionHostIdForRows,
|
||||
getProjectGroupExecutionHostIdForRows
|
||||
} from './worktree-list-host-filtering'
|
||||
import { getFolderWorkspaceCardPrDisplay } from './folder-workspace-card-pr-display'
|
||||
|
||||
export {
|
||||
getScrollTopToRevealBounds,
|
||||
@@ -612,6 +614,7 @@ type VirtualizedWorktreeViewportProps = {
|
||||
repoMap: Map<string, Repo>
|
||||
worktreeMap: Map<string, Worktree>
|
||||
worktreeLineageById: Record<string, WorktreeLineage>
|
||||
workspaceLineageByChildKey: Record<string, WorkspaceLineage>
|
||||
repoOrder: Map<string, number>
|
||||
// The full canonical state.repos id ordering — the drag controller commits
|
||||
// permutations of this list, even when some repos aren't currently visible
|
||||
@@ -621,6 +624,7 @@ type VirtualizedWorktreeViewportProps = {
|
||||
onReorderHostSections: (orderedHostIds: ExecutionHostId[]) => void
|
||||
onHostDragActiveChange: (active: boolean) => void
|
||||
prCache: Record<string, unknown> | null
|
||||
hostedReviewCache: Record<string, unknown> | null
|
||||
workspaceStatuses: readonly WorkspaceStatusDefinition[]
|
||||
projectGrouping?: ProjectGroupingModel
|
||||
projectGroups?: readonly ProjectGroup[]
|
||||
@@ -1237,11 +1241,13 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
||||
repoMap,
|
||||
worktreeMap,
|
||||
worktreeLineageById,
|
||||
workspaceLineageByChildKey,
|
||||
repoOrder,
|
||||
allRepoIds,
|
||||
onReorderHostSections,
|
||||
onHostDragActiveChange,
|
||||
prCache,
|
||||
hostedReviewCache,
|
||||
workspaceStatuses,
|
||||
projectGrouping,
|
||||
projectGroups = EMPTY_PROJECT_GROUPS,
|
||||
@@ -4760,6 +4766,16 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
||||
folderWorkspacePathStatus?.exists === false &&
|
||||
(isConfirmedStaleFolderPathStatus(folderWorkspacePathStatus) ||
|
||||
folderWorkspacePathStatus.reason === 'ambiguous-connection')
|
||||
const folderPrDisplay = getFolderWorkspaceCardPrDisplay({
|
||||
folderWorkspaceId: folderWorkspaceRow.folderWorkspace.id,
|
||||
workspaceLineageByChildKey,
|
||||
worktreeLineageById,
|
||||
worktreeMap,
|
||||
repoMap,
|
||||
hostedReviewCache,
|
||||
prCache,
|
||||
settings
|
||||
})
|
||||
const isFolderBackedWorkspaceChild =
|
||||
groupBy === 'repo' && folderWorkspaceRow.projectGroup.createdFrom === 'folder-scan'
|
||||
const { surfaceInset, cardContentIndent } = getFolderWorkspaceRowGeometry({
|
||||
@@ -4810,6 +4826,7 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
||||
activationRowKey={folderWorktree.id}
|
||||
onSelectionGesture={onSelectionGesture}
|
||||
onContextMenuSelect={onContextMenuSelect}
|
||||
statusPrDisplay={folderPrDisplay}
|
||||
/>
|
||||
<div className="pointer-events-auto absolute right-3 top-1.5">
|
||||
<FolderPathStatusIndicator status={folderWorkspacePathStatus} />
|
||||
@@ -4894,6 +4911,7 @@ const WorktreeList = React.memo(function WorktreeList({
|
||||
const repoMap = useRepoMap()
|
||||
const worktreeMap = useWorktreeMap()
|
||||
const worktreeLineageById = useAppStore((s) => s.worktreeLineageById)
|
||||
const workspaceLineageByChildKey = useAppStore((s) => s.workspaceLineageByChildKey)
|
||||
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
|
||||
const detectedWorktreesByRepo = useAppStore((s) => s.detectedWorktreesByRepo)
|
||||
const activeWorktreeId = useAppStore((s) => s.activeWorktreeId)
|
||||
@@ -5001,6 +5019,11 @@ const WorktreeList = React.memo(function WorktreeList({
|
||||
? s.prCache
|
||||
: null
|
||||
)
|
||||
const hostedReviewCache = useAppStore((s) =>
|
||||
s.settings?.experimentalNewWorktreeCardStyle === true && cardProps.includes('status')
|
||||
? s.hostedReviewCache
|
||||
: null
|
||||
)
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const sshTargetLabels = useAppStore((s) => s.sshTargetLabels)
|
||||
const sshConnectionStates = useAppStore((s) => s.sshConnectionStates)
|
||||
@@ -6401,11 +6424,13 @@ const WorktreeList = React.memo(function WorktreeList({
|
||||
repoMap={repoMap}
|
||||
worktreeMap={worktreeMap}
|
||||
worktreeLineageById={worktreeLineageById}
|
||||
workspaceLineageByChildKey={workspaceLineageByChildKey}
|
||||
repoOrder={repoOrder}
|
||||
allRepoIds={allRepoIds}
|
||||
onReorderHostSections={handleReorderHostSections}
|
||||
onHostDragActiveChange={setHostDragActive}
|
||||
prCache={prCache}
|
||||
hostedReviewCache={hostedReviewCache}
|
||||
workspaceStatuses={workspaceStatuses}
|
||||
projectGrouping={projectGrouping}
|
||||
projectGroups={projectGroups}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type {
|
||||
CheckStatus,
|
||||
Repo,
|
||||
Worktree,
|
||||
WorktreeLineage,
|
||||
WorkspaceLineage
|
||||
} from '../../../../shared/types'
|
||||
import { folderWorkspaceKey, worktreeWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import { getFolderWorkspaceCardPrDisplay } from './folder-workspace-card-pr-display'
|
||||
|
||||
const repo: Repo = {
|
||||
id: 'repo-1',
|
||||
path: '/repo',
|
||||
displayName: 'repo',
|
||||
badgeColor: '#999999',
|
||||
addedAt: 1
|
||||
}
|
||||
|
||||
function makeWorktree(overrides: Partial<Worktree> & { id: string }): Worktree {
|
||||
const { id, ...rest } = overrides
|
||||
return {
|
||||
id,
|
||||
repoId: repo.id,
|
||||
path: `/worktrees/${id}`,
|
||||
displayName: id,
|
||||
branch: `refs/heads/${id}`,
|
||||
head: 'abc123',
|
||||
isBare: false,
|
||||
isMainWorktree: false,
|
||||
comment: '',
|
||||
linkedIssue: null,
|
||||
linkedPR: null,
|
||||
linkedLinearIssue: null,
|
||||
linkedGitLabMR: null,
|
||||
linkedGitLabIssue: null,
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
lastActivityAt: 0,
|
||||
...rest
|
||||
}
|
||||
}
|
||||
|
||||
function makeWorkspaceLineage(worktree: Worktree): WorkspaceLineage {
|
||||
return {
|
||||
childWorkspaceKey: worktreeWorkspaceKey(worktree.id),
|
||||
childInstanceId: worktree.instanceId ?? null,
|
||||
parentWorkspaceKey: folderWorkspaceKey('folder-1'),
|
||||
parentInstanceId: null,
|
||||
origin: 'cli',
|
||||
capture: { source: 'env-workspace', confidence: 'inferred' },
|
||||
createdAt: 1
|
||||
}
|
||||
}
|
||||
|
||||
function makeWorktreeLineage(child: Worktree, parent: Worktree): WorktreeLineage {
|
||||
return {
|
||||
worktreeId: child.id,
|
||||
worktreeInstanceId: child.instanceId ?? '',
|
||||
parentWorktreeId: parent.id,
|
||||
parentWorktreeInstanceId: parent.instanceId ?? '',
|
||||
origin: 'cli',
|
||||
capture: { source: 'env-workspace', confidence: 'inferred' },
|
||||
createdAt: 1
|
||||
}
|
||||
}
|
||||
|
||||
describe('getFolderWorkspaceCardPrDisplay', () => {
|
||||
it('uses failing attached PR status ahead of pending and passing PRs', () => {
|
||||
const passing = makeWorktree({ id: 'passing', linkedPR: 1 })
|
||||
const pending = makeWorktree({ id: 'pending', linkedPR: 2 })
|
||||
const failing = makeWorktree({ id: 'failing', linkedPR: 3 })
|
||||
|
||||
const display = getFolderWorkspaceCardPrDisplay({
|
||||
folderWorkspaceId: 'folder-1',
|
||||
workspaceLineageByChildKey: {
|
||||
[passing.id]: makeWorkspaceLineage(passing),
|
||||
[pending.id]: makeWorkspaceLineage(pending),
|
||||
[failing.id]: makeWorkspaceLineage(failing)
|
||||
},
|
||||
worktreeLineageById: {},
|
||||
worktreeMap: new Map([
|
||||
[passing.id, passing],
|
||||
[pending.id, pending],
|
||||
[failing.id, failing]
|
||||
]),
|
||||
repoMap: new Map([[repo.id, repo]]),
|
||||
hostedReviewCache: null,
|
||||
prCache: {
|
||||
'repo-1::passing': { data: makePr(1, 'success') },
|
||||
'repo-1::pending': { data: makePr(2, 'pending') },
|
||||
'repo-1::failing': { data: makePr(3, 'failure') }
|
||||
}
|
||||
})
|
||||
|
||||
expect(display).toMatchObject({ number: 3, status: 'failure' })
|
||||
})
|
||||
|
||||
it('uses pending attached PR status ahead of passing PRs', () => {
|
||||
const passing = makeWorktree({ id: 'passing', linkedPR: 1 })
|
||||
const pending = makeWorktree({ id: 'pending', linkedPR: 2 })
|
||||
|
||||
const display = getFolderWorkspaceCardPrDisplay({
|
||||
folderWorkspaceId: 'folder-1',
|
||||
workspaceLineageByChildKey: {
|
||||
[passing.id]: makeWorkspaceLineage(passing),
|
||||
[pending.id]: makeWorkspaceLineage(pending)
|
||||
},
|
||||
worktreeLineageById: {},
|
||||
worktreeMap: new Map([
|
||||
[passing.id, passing],
|
||||
[pending.id, pending]
|
||||
]),
|
||||
repoMap: new Map([[repo.id, repo]]),
|
||||
hostedReviewCache: null,
|
||||
prCache: {
|
||||
'repo-1::passing': { data: makePr(1, 'success') },
|
||||
'repo-1::pending': { data: makePr(2, 'pending') }
|
||||
}
|
||||
})
|
||||
|
||||
expect(display).toMatchObject({ number: 2, status: 'pending' })
|
||||
})
|
||||
|
||||
it('includes nested attached worktree PRs', () => {
|
||||
const parent = makeWorktree({ id: 'parent', instanceId: 'parent' })
|
||||
const nested = makeWorktree({ id: 'nested', instanceId: 'nested', linkedPR: 4 })
|
||||
|
||||
const display = getFolderWorkspaceCardPrDisplay({
|
||||
folderWorkspaceId: 'folder-1',
|
||||
workspaceLineageByChildKey: { [parent.id]: makeWorkspaceLineage(parent) },
|
||||
worktreeLineageById: { [nested.id]: makeWorktreeLineage(nested, parent) },
|
||||
worktreeMap: new Map([
|
||||
[parent.id, parent],
|
||||
[nested.id, nested]
|
||||
]),
|
||||
repoMap: new Map([[repo.id, repo]]),
|
||||
hostedReviewCache: null,
|
||||
prCache: {
|
||||
'repo-1::nested': { data: makePr(4, 'success') }
|
||||
}
|
||||
})
|
||||
|
||||
expect(display).toMatchObject({ number: 4, status: 'success' })
|
||||
})
|
||||
})
|
||||
|
||||
function makePr(number: number, checksStatus: CheckStatus) {
|
||||
return {
|
||||
number,
|
||||
title: `PR ${number}`,
|
||||
state: 'open',
|
||||
url: `https://example.test/pull/${number}`,
|
||||
checksStatus,
|
||||
updatedAt: '2026-01-01T00:00:00.000Z',
|
||||
mergeable: 'UNKNOWN'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
import type { AppState } from '@/store/types'
|
||||
import { getHostedReviewCacheKey } from '@/store/slices/hosted-review'
|
||||
import { getGitHubPRCacheKey, getLegacyGitHubPRCacheKey } from '@/store/slices/github-cache-key'
|
||||
import { branchName } from '@/lib/git-utils'
|
||||
import type { HostedReviewInfo } from '../../../../shared/hosted-review'
|
||||
import type {
|
||||
PRInfo,
|
||||
Repo,
|
||||
Worktree,
|
||||
WorktreeLineage,
|
||||
WorkspaceLineage
|
||||
} from '../../../../shared/types'
|
||||
import { folderWorkspaceKey, parseWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import { getWorktreeCardPrDisplay, type WorktreeCardPrDisplay } from './worktree-card-pr-display'
|
||||
|
||||
type HostedReviewCacheEntry = {
|
||||
data?: HostedReviewInfo | null
|
||||
linkedReviewHintKey?: string
|
||||
}
|
||||
|
||||
type PrCacheEntry = {
|
||||
data?: PRInfo | null
|
||||
}
|
||||
|
||||
type FolderWorkspaceCardPrDisplayArgs = {
|
||||
folderWorkspaceId: string
|
||||
workspaceLineageByChildKey: Record<string, WorkspaceLineage> | null | undefined
|
||||
worktreeLineageById: Record<string, WorktreeLineage> | null | undefined
|
||||
worktreeMap: ReadonlyMap<string, Worktree>
|
||||
repoMap: ReadonlyMap<string, Repo>
|
||||
hostedReviewCache: Record<string, unknown> | null
|
||||
prCache: Record<string, unknown> | null
|
||||
settings?: AppState['settings']
|
||||
}
|
||||
|
||||
const REVIEW_STATUS_PRIORITY: Record<NonNullable<WorktreeCardPrDisplay['status']>, number> = {
|
||||
failure: 0,
|
||||
pending: 1,
|
||||
success: 2,
|
||||
neutral: 3
|
||||
}
|
||||
|
||||
export function getFolderWorkspaceCardPrDisplay({
|
||||
folderWorkspaceId,
|
||||
workspaceLineageByChildKey,
|
||||
worktreeLineageById,
|
||||
worktreeMap,
|
||||
repoMap,
|
||||
hostedReviewCache,
|
||||
prCache,
|
||||
settings
|
||||
}: FolderWorkspaceCardPrDisplayArgs): WorktreeCardPrDisplay | null {
|
||||
const reviews = getAttachedWorktreesForFolderWorkspaceCard({
|
||||
folderWorkspaceId,
|
||||
workspaceLineageByChildKey,
|
||||
worktreeLineageById,
|
||||
worktreeMap
|
||||
})
|
||||
.map((worktree) =>
|
||||
getAttachedWorktreePrDisplay({
|
||||
worktree,
|
||||
repo: repoMap.get(worktree.repoId),
|
||||
hostedReviewCache,
|
||||
prCache,
|
||||
settings
|
||||
})
|
||||
)
|
||||
.filter((review): review is WorktreeCardPrDisplay => review !== null)
|
||||
|
||||
if (reviews.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return reviews.sort(compareReviewDisplays)[0] ?? null
|
||||
}
|
||||
|
||||
function getAttachedWorktreesForFolderWorkspaceCard({
|
||||
folderWorkspaceId,
|
||||
workspaceLineageByChildKey,
|
||||
worktreeLineageById,
|
||||
worktreeMap
|
||||
}: Pick<
|
||||
FolderWorkspaceCardPrDisplayArgs,
|
||||
'folderWorkspaceId' | 'workspaceLineageByChildKey' | 'worktreeLineageById' | 'worktreeMap'
|
||||
>): Worktree[] {
|
||||
const folderKey = folderWorkspaceKey(folderWorkspaceId)
|
||||
const directChildren = Object.values(workspaceLineageByChildKey ?? {})
|
||||
.filter((lineage) => lineage.parentWorkspaceKey === folderKey)
|
||||
.map((lineage) => getWorkspaceLineageChild(lineage, worktreeMap))
|
||||
.filter((worktree): worktree is Worktree => worktree !== null)
|
||||
|
||||
const included = new Map(directChildren.map((worktree) => [worktree.id, worktree]))
|
||||
let added = true
|
||||
|
||||
while (added) {
|
||||
added = false
|
||||
for (const lineage of Object.values(worktreeLineageById ?? {})) {
|
||||
if (included.has(lineage.worktreeId) || !included.has(lineage.parentWorktreeId)) {
|
||||
continue
|
||||
}
|
||||
const parent = worktreeMap.get(lineage.parentWorktreeId)
|
||||
const child = worktreeMap.get(lineage.worktreeId)
|
||||
if (!isCurrentLineagePair(parent, child, lineage)) {
|
||||
continue
|
||||
}
|
||||
included.set(child.id, child)
|
||||
added = true
|
||||
}
|
||||
}
|
||||
|
||||
return [...included.values()]
|
||||
}
|
||||
|
||||
function getWorkspaceLineageChild(
|
||||
lineage: WorkspaceLineage,
|
||||
worktreeMap: ReadonlyMap<string, Worktree>
|
||||
): Worktree | null {
|
||||
const childScope = parseWorkspaceKey(lineage.childWorkspaceKey)
|
||||
if (childScope?.type !== 'worktree') {
|
||||
return null
|
||||
}
|
||||
const worktree = worktreeMap.get(childScope.worktreeId)
|
||||
if (!worktree || worktree.isArchived) {
|
||||
return null
|
||||
}
|
||||
if (lineage.childInstanceId && lineage.childInstanceId !== worktree.instanceId) {
|
||||
return null
|
||||
}
|
||||
return worktree
|
||||
}
|
||||
|
||||
function isCurrentLineagePair(
|
||||
parent: Worktree | undefined,
|
||||
child: Worktree | undefined,
|
||||
lineage: WorktreeLineage
|
||||
): child is Worktree {
|
||||
return Boolean(
|
||||
parent &&
|
||||
child &&
|
||||
!parent.isArchived &&
|
||||
!child.isArchived &&
|
||||
child.instanceId === lineage.worktreeInstanceId &&
|
||||
parent.instanceId === lineage.parentWorktreeInstanceId
|
||||
)
|
||||
}
|
||||
|
||||
function getAttachedWorktreePrDisplay({
|
||||
worktree,
|
||||
repo,
|
||||
hostedReviewCache,
|
||||
prCache,
|
||||
settings
|
||||
}: {
|
||||
worktree: Worktree
|
||||
repo: Repo | undefined
|
||||
hostedReviewCache: Record<string, unknown> | null
|
||||
prCache: Record<string, unknown> | null
|
||||
settings?: AppState['settings']
|
||||
}): WorktreeCardPrDisplay | null {
|
||||
if (!repo) {
|
||||
return null
|
||||
}
|
||||
|
||||
const branch = branchName(worktree.branch).trim()
|
||||
if (!branch) {
|
||||
return getLinkedReviewDisplay(worktree)
|
||||
}
|
||||
|
||||
const hostedReviewCacheKey = getHostedReviewCacheKey(
|
||||
repo.path,
|
||||
branch,
|
||||
settings,
|
||||
repo.id,
|
||||
repo.connectionId,
|
||||
repo.executionHostId
|
||||
)
|
||||
const hostedReviewEntry = hostedReviewCache?.[hostedReviewCacheKey] as
|
||||
| HostedReviewCacheEntry
|
||||
| undefined
|
||||
const hostedReviewDisplay = hostedReviewEntry
|
||||
? getWorktreeCardPrDisplay(
|
||||
hostedReviewEntry.data,
|
||||
worktree.linkedPR,
|
||||
worktree.linkedGitLabMR ?? null,
|
||||
worktree.linkedBitbucketPR ?? null,
|
||||
worktree.linkedAzureDevOpsPR ?? null,
|
||||
worktree.linkedGiteaPR ?? null,
|
||||
{ reviewHintKey: hostedReviewEntry.linkedReviewHintKey }
|
||||
)
|
||||
: null
|
||||
if (hostedReviewDisplay) {
|
||||
return hostedReviewDisplay
|
||||
}
|
||||
|
||||
const cachedGitHubPr = getCachedGitHubPr({ worktree, repo, branch, prCache, settings })
|
||||
if (cachedGitHubPr) {
|
||||
return {
|
||||
provider: 'github',
|
||||
number: cachedGitHubPr.number,
|
||||
title: cachedGitHubPr.title,
|
||||
state: cachedGitHubPr.state,
|
||||
url: cachedGitHubPr.url,
|
||||
status: cachedGitHubPr.checksStatus
|
||||
}
|
||||
}
|
||||
|
||||
return getLinkedReviewDisplay(worktree)
|
||||
}
|
||||
|
||||
function getLinkedReviewDisplay(worktree: Worktree): WorktreeCardPrDisplay | null {
|
||||
return getWorktreeCardPrDisplay(
|
||||
undefined,
|
||||
worktree.linkedPR,
|
||||
worktree.linkedGitLabMR ?? null,
|
||||
worktree.linkedBitbucketPR ?? null,
|
||||
worktree.linkedAzureDevOpsPR ?? null,
|
||||
worktree.linkedGiteaPR ?? null
|
||||
)
|
||||
}
|
||||
|
||||
function getCachedGitHubPr({
|
||||
worktree,
|
||||
repo,
|
||||
branch,
|
||||
prCache,
|
||||
settings
|
||||
}: {
|
||||
worktree: Worktree
|
||||
repo: Repo
|
||||
branch: string
|
||||
prCache: Record<string, unknown> | null
|
||||
settings?: AppState['settings']
|
||||
}): PRInfo | null {
|
||||
if (!prCache || worktree.linkedPR === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const cacheKey = getGitHubPRCacheKey(
|
||||
repo.path,
|
||||
repo.id,
|
||||
branch,
|
||||
settings,
|
||||
repo.connectionId,
|
||||
repo.executionHostId
|
||||
)
|
||||
const canUseLegacyPRCache =
|
||||
!settings?.activeRuntimeEnvironmentId?.trim() && !repo.connectionId && !repo.executionHostId
|
||||
const legacyRepoScopedCacheKey = canUseLegacyPRCache
|
||||
? getLegacyGitHubPRCacheKey(repo.path, repo.id, branch)
|
||||
: ''
|
||||
const legacyPathScopedCacheKey = canUseLegacyPRCache
|
||||
? getLegacyGitHubPRCacheKey(repo.path, undefined, branch)
|
||||
: ''
|
||||
const entry =
|
||||
(prCache[cacheKey] as PrCacheEntry | undefined) ??
|
||||
(legacyRepoScopedCacheKey
|
||||
? (prCache[legacyRepoScopedCacheKey] as PrCacheEntry | undefined)
|
||||
: undefined) ??
|
||||
(legacyPathScopedCacheKey
|
||||
? (prCache[legacyPathScopedCacheKey] as PrCacheEntry | undefined)
|
||||
: undefined)
|
||||
const pr = entry?.data ?? null
|
||||
return pr?.number === worktree.linkedPR ? pr : null
|
||||
}
|
||||
|
||||
function compareReviewDisplays(left: WorktreeCardPrDisplay, right: WorktreeCardPrDisplay): number {
|
||||
return getReviewDisplayPriority(left) - getReviewDisplayPriority(right)
|
||||
}
|
||||
|
||||
function getReviewDisplayPriority(review: WorktreeCardPrDisplay): number {
|
||||
return review.status ? REVIEW_STATUS_PRIORITY[review.status] : 4
|
||||
}
|
||||
Reference in New Issue
Block a user