mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 08:03:09 +00:00
Merge branch 'main' of https://github.com/stablyai/orca into brennanb2025/renderer-crash-loop-dampening
This commit is contained in:
@@ -30,6 +30,7 @@ vi.mock('@/store', () => ({
|
||||
linearIssueCache: {},
|
||||
openModal,
|
||||
openTaskPage: vi.fn(),
|
||||
projectGroups: [],
|
||||
ptyIdsByTabId: {},
|
||||
remoteBranchConflictByWorktreeId: {},
|
||||
renamingWorktreeId: null,
|
||||
|
||||
@@ -17,6 +17,7 @@ const setWorkspacePortScanRefreshing = vi.fn()
|
||||
|
||||
let worktreeCardProperties: WorktreeCardProperty[] = ['status', 'ports']
|
||||
let hostedReviewCache: Record<string, unknown> = {}
|
||||
let projectGroups: unknown[] = []
|
||||
let workspacePortScan: { key: string; result: WorkspacePortScanResult } | null = null
|
||||
let settings: Partial<GlobalSettings> | null = { compactWorktreeCards: true }
|
||||
|
||||
@@ -35,6 +36,7 @@ vi.mock('@/store', () => ({
|
||||
linearIssueCache: {},
|
||||
openModal,
|
||||
openTaskPage,
|
||||
projectGroups,
|
||||
ptyIdsByTabId: {},
|
||||
recordFeatureInteraction,
|
||||
remoteBranchConflictByWorktreeId: {},
|
||||
@@ -172,6 +174,7 @@ describe('WorktreeCard compact hover details', () => {
|
||||
vi.clearAllMocks()
|
||||
worktreeCardProperties = ['status', 'ports']
|
||||
hostedReviewCache = {}
|
||||
projectGroups = []
|
||||
workspacePortScan = null
|
||||
settings = { compactWorktreeCards: true }
|
||||
})
|
||||
@@ -561,6 +564,78 @@ describe('WorktreeCard compact hover details', () => {
|
||||
expect(markup).toContain('text-[13px] leading-5')
|
||||
})
|
||||
|
||||
it('does not show a folder path row in new-card mode when no project groups exist', async () => {
|
||||
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: true }
|
||||
worktreeCardProperties = ['status', 'branch']
|
||||
const { default: WorktreeCard } = await import('./WorktreeCard')
|
||||
|
||||
const markup = renderToStaticMarkup(
|
||||
<WorktreeCard
|
||||
worktree={makeWorktree({
|
||||
displayName: 'Docs folder',
|
||||
branch: '',
|
||||
path: ' /Users/x/projects/my-app '
|
||||
})}
|
||||
repo={{ ...makeRepo(), kind: 'folder' }}
|
||||
isActive={false}
|
||||
hideRepoBadge
|
||||
/>
|
||||
)
|
||||
|
||||
expect(markup).not.toContain('data-worktree-card-meta-row=""')
|
||||
expect(markup).toContain('Docs folder')
|
||||
expect(markup).not.toContain('/Users/x/projects/my-app')
|
||||
})
|
||||
|
||||
it('shows a folder path row in new-card mode through the branch setting when project groups exist', async () => {
|
||||
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: true }
|
||||
worktreeCardProperties = ['status', 'branch']
|
||||
projectGroups = [{ id: 'group-1' }]
|
||||
const { default: WorktreeCard } = await import('./WorktreeCard')
|
||||
|
||||
const markup = renderToStaticMarkup(
|
||||
<WorktreeCard
|
||||
worktree={makeWorktree({
|
||||
displayName: 'Docs folder',
|
||||
branch: '',
|
||||
path: ' /Users/x/projects/my-app '
|
||||
})}
|
||||
repo={{ ...makeRepo(), kind: 'folder' }}
|
||||
isActive={false}
|
||||
hideRepoBadge
|
||||
/>
|
||||
)
|
||||
|
||||
expect(markup).toContain('data-worktree-card-meta-row=""')
|
||||
expect(markup).toContain(' /Users/x/projects/my-app ')
|
||||
expect(markup).not.toContain('>Folder</span>')
|
||||
})
|
||||
|
||||
it('keeps hidden folder path identity available from the new-card hover', async () => {
|
||||
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: true }
|
||||
worktreeCardProperties = ['status']
|
||||
projectGroups = [{ id: 'group-1' }]
|
||||
const { default: WorktreeCard } = await import('./WorktreeCard')
|
||||
|
||||
const markup = renderToStaticMarkup(
|
||||
<WorktreeCard
|
||||
worktree={makeWorktree({
|
||||
displayName: 'Docs folder',
|
||||
branch: '',
|
||||
path: '/Users/x/projects/my-app'
|
||||
})}
|
||||
repo={{ ...makeRepo(), kind: 'folder' }}
|
||||
isActive={false}
|
||||
hideRepoBadge
|
||||
/>
|
||||
)
|
||||
|
||||
expect(markup).not.toContain('data-worktree-card-meta-row=""')
|
||||
expect(markup).toContain('data-hover-open-delay="100"')
|
||||
expect(markup).toContain('/Users/x/projects/my-app')
|
||||
expect(markup).toContain('Docs folder')
|
||||
})
|
||||
|
||||
it('shows the branch row for migrated Default cards with branch enabled', async () => {
|
||||
settings = { compactWorktreeCards: false }
|
||||
worktreeCardProperties = ['status', 'branch']
|
||||
|
||||
@@ -33,6 +33,7 @@ vi.mock('@/store', () => ({
|
||||
issueCache: {},
|
||||
linearIssueCache: {},
|
||||
openModal,
|
||||
projectGroups: [],
|
||||
remoteBranchConflictByWorktreeId: {},
|
||||
settings: { experimentalNewWorktreeCardStyle: true },
|
||||
sshConnectionStates: new Map(),
|
||||
|
||||
@@ -24,6 +24,7 @@ vi.mock('@/store', () => ({
|
||||
issueCache: {},
|
||||
linearIssueCache: {},
|
||||
openModal,
|
||||
projectGroups: [],
|
||||
remoteBranchConflictByWorktreeId: {},
|
||||
settings: null,
|
||||
sshConnectionStates: new Map(),
|
||||
|
||||
@@ -25,6 +25,7 @@ vi.mock('@/store', () => ({
|
||||
issueCache: {},
|
||||
linearIssueCache: {},
|
||||
openModal,
|
||||
projectGroups: [],
|
||||
remoteBranchConflictByWorktreeId: {},
|
||||
settings,
|
||||
sshConnectionStates: new Map(),
|
||||
|
||||
@@ -29,6 +29,7 @@ vi.mock('@/store', () => ({
|
||||
issueCache: {},
|
||||
linearIssueCache: {},
|
||||
openModal,
|
||||
projectGroups: [],
|
||||
remoteBranchConflictByWorktreeId: {},
|
||||
settings,
|
||||
sshConnectionStates: new Map(),
|
||||
|
||||
@@ -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,6 +36,7 @@ vi.mock('@/store', () => ({
|
||||
hostedReviewCache: {},
|
||||
issueCache: {},
|
||||
openModal,
|
||||
projectGroups,
|
||||
remoteBranchConflictByWorktreeId: {},
|
||||
settings,
|
||||
sshConnectionStates: new Map(),
|
||||
@@ -143,6 +145,7 @@ describe('WorktreeCard quick actions', () => {
|
||||
ptyIdsByTabId = {}
|
||||
browserTabsByWorktree = {}
|
||||
settings = null
|
||||
projectGroups = []
|
||||
workspaceDeleteModifierPressed = false
|
||||
gitConflictOperationByWorktree = {}
|
||||
})
|
||||
@@ -181,7 +184,7 @@ describe('WorktreeCard quick actions', () => {
|
||||
expect(markup).not.toContain('bg-black/[0.08]')
|
||||
})
|
||||
|
||||
it('renders folder kind and directory in the detailed metadata row', () => {
|
||||
it('renders folder directory name in the detailed metadata row without a Folder badge', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<WorktreeCard
|
||||
worktree={makeWorktree({ displayName: 'Docs folder', branch: '' })}
|
||||
@@ -191,12 +194,12 @@ describe('WorktreeCard quick actions', () => {
|
||||
)
|
||||
|
||||
expect(markup).toContain('Docs folder')
|
||||
expect(markup).toContain('>Folder</span>')
|
||||
expect(markup).not.toContain('>Folder</span>')
|
||||
expect(markup).toContain('>quick-action</span>')
|
||||
expect(markup).toContain('data-worktree-card-meta-row=""')
|
||||
})
|
||||
|
||||
it('renders synthetic folder workspace directory in the detailed metadata row', () => {
|
||||
it('renders synthetic folder workspace directory name in the detailed metadata row without a Folder badge', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<WorktreeCard
|
||||
worktree={makeWorktree({
|
||||
@@ -211,11 +214,33 @@ describe('WorktreeCard quick actions', () => {
|
||||
)
|
||||
|
||||
expect(markup).toContain('Docs folder')
|
||||
expect(markup).toContain('>Folder</span>')
|
||||
expect(markup).not.toContain('>Folder</span>')
|
||||
expect(markup).toContain('>quick-action</span>')
|
||||
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
|
||||
|
||||
@@ -28,6 +28,7 @@ vi.mock('@/store', () => ({
|
||||
issueCache: {},
|
||||
linearIssueCache: {},
|
||||
openModal,
|
||||
projectGroups: [],
|
||||
remoteBranchConflictByWorktreeId: {},
|
||||
runtimeStatusByEnvironmentId,
|
||||
settings: null,
|
||||
|
||||
@@ -26,7 +26,7 @@ import WorktreeCardAgents from './WorktreeCardAgents'
|
||||
import { WorktreeCardStatusSlot } from './WorktreeCardStatusSlot'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { activateWorktreeFromSidebar } from '@/lib/sidebar-worktree-activation'
|
||||
import { getRepoKindLabel, isFolderRepo } from '../../../../shared/repo-kind'
|
||||
import { isFolderRepo } from '../../../../shared/repo-kind'
|
||||
import type { HostedReviewInfo } from '../../../../shared/hosted-review'
|
||||
import type {
|
||||
GitHubWorkItem,
|
||||
@@ -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)
|
||||
@@ -228,6 +231,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
const fetchIssue = useAppStore((s) => s.fetchIssue)
|
||||
const fetchLinearIssue = useAppStore((s) => s.fetchLinearIssue)
|
||||
const cardProps = useAppStore((s) => s.worktreeCardProperties)
|
||||
const projectGroups = useAppStore((s) => s.projectGroups)
|
||||
const newCardStyle = settings?.experimentalNewWorktreeCardStyle === true
|
||||
const compactCards = !newCardStyle && settings?.compactWorktreeCards === true
|
||||
const activeSurfaceIsSecondary = isActiveSurface && activeSurfaceVariant === 'secondary'
|
||||
@@ -367,6 +371,18 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
const folderWorkspaceId =
|
||||
workspaceScope?.type === 'folder' ? workspaceScope.folderWorkspaceId : null
|
||||
const isFolder = repo ? isFolderRepo(repo) : folderWorkspaceId !== null
|
||||
// Why: project groups are the product gate for folder workspaces, so folder
|
||||
// paths stay hidden from identity surfaces until that capability exists.
|
||||
const hasProjectGroups = projectGroups.length > 0
|
||||
const branchIdentityDisplay = !isFolder && branch.length > 0 ? branch : undefined
|
||||
const folderPathIdentityDisplay =
|
||||
isFolder && hasProjectGroups && worktree.path.trim().length > 0 ? worktree.path : undefined
|
||||
const identityDisplay = branchIdentityDisplay ?? folderPathIdentityDisplay
|
||||
const hasPathIdentityEnabled = cardProps.includes('branch')
|
||||
const showIdentityInNewCard = newCardStyle && hasPathIdentityEnabled && Boolean(identityDisplay)
|
||||
const folderMetaRowContent = newCardStyle
|
||||
? hasPathIdentityEnabled && Boolean(folderPathIdentityDisplay)
|
||||
: isFolder
|
||||
const hostedReviewCacheKey =
|
||||
repo && branch
|
||||
? getHostedReviewCacheKey(
|
||||
@@ -936,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
|
||||
@@ -1051,7 +1068,8 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
const showBranch =
|
||||
!isFolder &&
|
||||
branch.length > 0 &&
|
||||
(newCardStyle ? cardProps.includes('branch') : !compactCards || branch !== worktree.displayName)
|
||||
!newCardStyle &&
|
||||
(!compactCards || branch !== worktree.displayName)
|
||||
// Why: rebases already surface in source control; keep dense cards from
|
||||
// carrying a persistent rebase chip while preserving other interruption cues.
|
||||
const showConflictOperationBadge =
|
||||
@@ -1070,30 +1088,28 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
const hasDetailedMetaRowContent = Boolean(
|
||||
(showRepoBadgeInMetaRow && repo) ||
|
||||
showHostContextBadge ||
|
||||
isFolder ||
|
||||
folderMetaRowContent ||
|
||||
showBranch ||
|
||||
showIdentityInNewCard ||
|
||||
showDetachedHeadInMetaRow ||
|
||||
showConflictOperationBadge ||
|
||||
cacheStartedAt != null ||
|
||||
showMetaRowDetails
|
||||
)
|
||||
const hasMetaRow = compactCards
|
||||
? hasMetadataBadge || (newCardStyle && showBranch) || cacheStartedAt != null
|
||||
? hasMetadataBadge || cacheStartedAt != null
|
||||
: hasDetailedMetaRowContent
|
||||
const showHeaderActions = showTitleRowPrimary || showDeleteQuickAction
|
||||
// Why: the hover owns full identity when the row truncates; normalize once
|
||||
// so title/branch de-dupe and identity-only hover eligibility stay in sync.
|
||||
const trimmedVisibleCardTitle = visibleCardTitle.trim()
|
||||
const showBranchIdentityHover = newCardStyle
|
||||
? !isFolder &&
|
||||
branch.length > 0 &&
|
||||
? Boolean(identityDisplay) &&
|
||||
!cardProps.includes('branch') &&
|
||||
branch !== trimmedVisibleCardTitle
|
||||
identityDisplay !== trimmedVisibleCardTitle
|
||||
: compactCards && showBranch
|
||||
const hoverBranchName = newCardStyle
|
||||
? !isFolder && branch.length > 0
|
||||
? branch
|
||||
: undefined
|
||||
? identityDisplay
|
||||
: showBranchIdentityHover
|
||||
? branch
|
||||
: undefined
|
||||
@@ -1257,9 +1273,9 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
unreadTooltip={unreadTooltip}
|
||||
onPointerDown={stopQuickActionPointerPropagation}
|
||||
onToggleUnread={handleToggleUnreadQuick}
|
||||
prDisplay={hoverReview}
|
||||
prDisplay={statusLaneReview}
|
||||
newCardStyle={newCardStyle}
|
||||
hasBranchIdentity={!isFolder && branch.length > 0}
|
||||
hasBranchIdentity={Boolean(branchIdentityDisplay)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -1372,17 +1388,6 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
}
|
||||
/>
|
||||
|
||||
{isFolder && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-[16px] px-1.5 text-[10px] font-medium rounded shrink-0 text-muted-foreground bg-accent border border-border dark:bg-accent/80 dark:border-border/50 leading-none"
|
||||
>
|
||||
{repo
|
||||
? getRepoKindLabel(repo)
|
||||
: translate('auto.components.sidebar.WorktreeCard.93aebe4529', 'Folder')}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{typeof worktree.firstAgentMessageRenameError === 'string' &&
|
||||
worktree.firstAgentMessageRenameError.length > 0 &&
|
||||
!titleRenaming ? (
|
||||
@@ -1542,7 +1547,13 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{isFolder ? (
|
||||
{showIdentityInNewCard ? (
|
||||
<TruncatedSidebarLabel
|
||||
text={identityDisplay!}
|
||||
className="text-[11px] text-muted-foreground leading-none"
|
||||
tooltipEnabled={!hasHoverDetails}
|
||||
/>
|
||||
) : isFolder && !newCardStyle ? (
|
||||
<span
|
||||
className="min-w-0 truncate font-mono text-[11px] leading-none text-muted-foreground"
|
||||
title={worktree.path}
|
||||
|
||||
@@ -11,6 +11,7 @@ const setWorktreeCardProperties = vi.fn()
|
||||
const setAgentActivityDisplayMode = vi.fn()
|
||||
|
||||
let settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: false }
|
||||
let projectGroups: unknown[] = []
|
||||
let worktreeCardProperties = [
|
||||
'status',
|
||||
'unread',
|
||||
@@ -27,6 +28,7 @@ vi.mock('@/store', () => ({
|
||||
useAppStore: (selector: (state: unknown) => unknown) =>
|
||||
selector({
|
||||
agentActivityDisplayMode: 'compact',
|
||||
projectGroups,
|
||||
setAgentActivityDisplayMode,
|
||||
setWorktreeCardMode,
|
||||
setWorktreeCardProperties,
|
||||
@@ -112,6 +114,7 @@ function renderMenu(): void {
|
||||
|
||||
beforeEach(() => {
|
||||
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: false }
|
||||
projectGroups = []
|
||||
worktreeCardProperties = [
|
||||
'status',
|
||||
'unread',
|
||||
@@ -153,4 +156,23 @@ describe('WorktreeCardDisplayMenuSection', () => {
|
||||
|
||||
expect(setWorktreeCardMode).toHaveBeenCalledWith('Compact')
|
||||
})
|
||||
|
||||
it('keeps branch-only copy when project groups are unavailable', () => {
|
||||
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: true }
|
||||
|
||||
renderMenu()
|
||||
|
||||
expect(container?.textContent).toContain('Branch name')
|
||||
expect(container?.textContent).not.toContain('Branch / folder path')
|
||||
})
|
||||
|
||||
it('mentions folder paths when project groups can create folder workspaces', () => {
|
||||
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: true }
|
||||
projectGroups = [{ id: 'group-1' }]
|
||||
|
||||
renderMenu()
|
||||
|
||||
expect(container?.textContent).toContain('Branch / folder path')
|
||||
expect(container?.textContent).not.toContain('Branch name')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -32,6 +32,7 @@ export function WorktreeCardDisplayMenuSection({
|
||||
const setWorktreeCardMode = useAppStore((s) => s.setWorktreeCardMode)
|
||||
const agentActivityDisplayMode = useAppStore((s) => s.agentActivityDisplayMode)
|
||||
const setAgentActivityDisplayMode = useAppStore((s) => s.setAgentActivityDisplayMode)
|
||||
const projectGroups = useAppStore((s) => s.projectGroups)
|
||||
const newCardStyle = settings?.experimentalNewWorktreeCardStyle === true
|
||||
const cardLayout = settings?.compactWorktreeCards ? 'compact' : 'detailed'
|
||||
const cardLayoutLabel =
|
||||
@@ -39,9 +40,10 @@ export function WorktreeCardDisplayMenuSection({
|
||||
const visiblePropertyCount = PROPERTY_OPTIONS.filter((opt) =>
|
||||
worktreeCardProperties.includes(opt.id)
|
||||
).length
|
||||
const hasProjectGroups = projectGroups.length > 0
|
||||
const worktreeCardPropertyOptions = useMemo(
|
||||
() => getWorktreeCardPropertyOptions({ newCardStyle }),
|
||||
[newCardStyle]
|
||||
() => getWorktreeCardPropertyOptions({ newCardStyle, hasProjectGroups }),
|
||||
[newCardStyle, hasProjectGroups]
|
||||
)
|
||||
const handleWorktreeCardPropertyChange = useCallback(
|
||||
(properties: readonly WorktreeCardProperty[], checked: boolean): void => {
|
||||
|
||||
@@ -205,7 +205,7 @@ describe('WorktreeCardStatusSlot', () => {
|
||||
expect(markup).not.toContain('bg-neutral-500/40')
|
||||
})
|
||||
|
||||
it('uses a branch icon instead of the quiet active dot when new card style has no review', () => {
|
||||
it('uses a branch icon with branch-only tooltip copy by default', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<WorktreeCardStatusSlot
|
||||
worktreeId="wt-1"
|
||||
@@ -221,12 +221,33 @@ describe('WorktreeCardStatusSlot', () => {
|
||||
)
|
||||
|
||||
expect(markup).toContain('Branch')
|
||||
expect(markup).not.toContain('Branch or folder path')
|
||||
expect(markup).toContain('lucide-git-branch')
|
||||
expect(markup).toContain('size-[13px] translate-x-px text-muted-foreground/70')
|
||||
expect(markup).toContain('text-muted-foreground/70')
|
||||
expect(markup).not.toContain('bg-emerald-500')
|
||||
})
|
||||
|
||||
it('uses context-aware branch or folder path tooltip copy', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<WorktreeCardStatusSlot
|
||||
worktreeId="wt-1"
|
||||
showStatus
|
||||
showUnreadAction={false}
|
||||
isUnread={false}
|
||||
unreadTooltip="Mark as unread"
|
||||
onPointerDown={vi.fn()}
|
||||
onToggleUnread={vi.fn()}
|
||||
newCardStyle
|
||||
hasBranchIdentity
|
||||
branchIdentityLabel="Branch or folder path"
|
||||
/>
|
||||
)
|
||||
|
||||
expect(markup).toContain('Branch or folder path')
|
||||
expect(markup).toContain('lucide-git-branch')
|
||||
})
|
||||
|
||||
it('keeps the quiet dot when the row has no branch identity', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<WorktreeCardStatusSlot
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
import { Bell, GitBranch } from 'lucide-react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { getWorktreeStatusLabel, type WorktreeStatus } from '@/lib/worktree-status'
|
||||
import { FilledBellIcon } from './WorktreeCardHelpers'
|
||||
@@ -20,13 +21,16 @@ type WorktreeCardStatusSlotProps = {
|
||||
prDisplay?: WorktreeCardPrDisplay | null
|
||||
newCardStyle?: boolean
|
||||
hasBranchIdentity?: boolean
|
||||
branchIdentityLabel?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const QUIET_REVIEW_REPLACEABLE_STATUSES = new Set<WorktreeStatus>(['active', 'done', 'inactive'])
|
||||
// Why: a missing review display can also mean provider state is unavailable,
|
||||
// so the passive label names the branch cue without claiming no review exists.
|
||||
const BRANCH_STATUS_LABEL = 'Branch'
|
||||
// so the passive label names the identity cue without claiming no review exists.
|
||||
function getDefaultBranchIdentityLabel(): string {
|
||||
return translate('auto.components.sidebar.WorktreeCardStatusSlot.branchIdentity', 'Branch')
|
||||
}
|
||||
// Why: branch-style SVGs are optically left-heavy; this keeps them aligned with
|
||||
// the centered activity dots in the shared status column.
|
||||
const compactReviewAndBranchStatusIconClassName = 'size-[13px] translate-x-px'
|
||||
@@ -93,6 +97,7 @@ export function WorktreeCardStatusSlot({
|
||||
prDisplay = null,
|
||||
newCardStyle = false,
|
||||
hasBranchIdentity = false,
|
||||
branchIdentityLabel,
|
||||
className
|
||||
}: WorktreeCardStatusSlotProps): React.JSX.Element | null {
|
||||
const status = useWorktreeActivityStatus(worktreeId)
|
||||
@@ -112,7 +117,7 @@ export function WorktreeCardStatusSlot({
|
||||
canShowReviewStatus && prDisplay
|
||||
? getReviewStatusTooltip(prDisplay)
|
||||
: canShowBranchStatus
|
||||
? BRANCH_STATUS_LABEL
|
||||
? (branchIdentityLabel ?? getDefaultBranchIdentityLabel())
|
||||
: statusLabel
|
||||
const passiveStatusTooltip =
|
||||
newCardStyle && isUnread ? `${passiveStatusLabel} · Unread` : passiveStatusLabel
|
||||
|
||||
@@ -340,6 +340,7 @@ function makeFolderWorkspacePathStatusState(): Record<string, unknown> {
|
||||
function setFolderWorkspaceFixtureState(
|
||||
options: {
|
||||
createdFrom?: ProjectGroup['createdFrom']
|
||||
experimentalNewWorktreeCardStyle?: boolean
|
||||
nestedGroup?: boolean
|
||||
} = {}
|
||||
): void {
|
||||
@@ -405,7 +406,9 @@ function setFolderWorkspaceFixtureState(
|
||||
setRenamingWorktreeId: vi.fn(),
|
||||
setShowSleepingWorkspaces: vi.fn(),
|
||||
setSortBy: vi.fn(),
|
||||
settings: null,
|
||||
settings: options.experimentalNewWorktreeCardStyle
|
||||
? { experimentalNewWorktreeCardStyle: true }
|
||||
: null,
|
||||
renamingWorktreeId: null,
|
||||
showSleepingWorkspaces: true,
|
||||
sortBy: 'manual',
|
||||
@@ -1040,6 +1043,25 @@ describe('WorktreeList lineage child card renderer', () => {
|
||||
).toBe(20)
|
||||
})
|
||||
|
||||
it('uses comparable new-card worktree geometry for experimental folder workspace rows', async () => {
|
||||
setFolderWorkspaceFixtureState({ experimentalNewWorktreeCardStyle: true })
|
||||
const markup = await renderWorktreeListMarkup()
|
||||
const folderWorktreeId = folderWorkspaceKey('folder-workspace-1')
|
||||
const cardOpeningTag = getCardOpeningTag(markup, folderWorktreeId)
|
||||
const surfaceOpeningTag = getFolderWorkspaceSurfaceOpeningTag(markup, 'folder-workspace-1')
|
||||
const cardContentIndent = getDataNumber(cardOpeningTag, 'data-content-indent')
|
||||
|
||||
expect(surfaceOpeningTag).toContain('padding-left:14px')
|
||||
expect(cardOpeningTag).toContain('data-content-indent="16"')
|
||||
expect(cardOpeningTag).toContain('data-flush-surface="true"')
|
||||
expect(
|
||||
getFlushCardContentStart({
|
||||
cardContentIndent,
|
||||
surfaceInset: getPaddingLeft(surfaceOpeningTag)
|
||||
})
|
||||
).toBe(30)
|
||||
})
|
||||
|
||||
it('preserves manual folder workspace indentation outside folder-scanned groups', async () => {
|
||||
setFolderWorkspaceFixtureState({ createdFrom: 'manual' })
|
||||
const markup = await renderWorktreeListMarkup()
|
||||
|
||||
@@ -57,6 +57,7 @@ import type {
|
||||
ProjectOrderBy,
|
||||
WorktreeLineage,
|
||||
WorktreeMeta,
|
||||
WorkspaceLineage,
|
||||
WorkspaceStatus,
|
||||
WorkspaceStatusDefinition
|
||||
} from '../../../../shared/types'
|
||||
@@ -225,8 +226,7 @@ import {
|
||||
LINEAGE_CHILDREN_INLINE_OFFSET,
|
||||
getFolderBackedRepoWorktreeCardContentIndent,
|
||||
getFolderBackedRepoWorktreeCardSurfaceInset,
|
||||
getFolderWorkspaceCardContentIndent,
|
||||
getFolderWorkspaceCardSurfaceInset,
|
||||
getFolderWorkspaceRowGeometry,
|
||||
getLineageChildrenInlineStyle,
|
||||
getLineageNestedRowGeometry,
|
||||
getProjectGroupHeaderPaddingLeft,
|
||||
@@ -256,6 +256,7 @@ import {
|
||||
getFolderWorkspaceExecutionHostIdForRows,
|
||||
getProjectGroupExecutionHostIdForRows
|
||||
} from './worktree-list-host-filtering'
|
||||
import { getFolderWorkspaceCardPrDisplay } from './folder-workspace-card-pr-display'
|
||||
|
||||
export {
|
||||
getScrollTopToRevealBounds,
|
||||
@@ -613,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
|
||||
@@ -622,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[]
|
||||
@@ -1238,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,
|
||||
@@ -4761,30 +4766,25 @@ 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 contentIndent = isFolderBackedWorkspaceChild
|
||||
? getFolderWorkspaceCardContentIndent({
|
||||
groupDepth: folderWorkspaceRow.groupDepth
|
||||
})
|
||||
: getWorktreeCardContentIndent({
|
||||
isGrouped: groupBy !== 'none',
|
||||
groupDepth: folderWorkspaceRow.groupDepth,
|
||||
lineageDepth: folderWorkspaceRow.depth
|
||||
})
|
||||
// Why: folder workspace surfaces should step inward with their
|
||||
// project-group nesting, matching lineage child card surfaces
|
||||
// instead of spanning from the sidebar edge at every depth.
|
||||
const surfaceInset = isFolderBackedWorkspaceChild
|
||||
? getFolderWorkspaceCardSurfaceInset({
|
||||
isGrouped: true,
|
||||
groupDepth: folderWorkspaceRow.groupDepth
|
||||
})
|
||||
: getWorktreeCardSurfaceInset({
|
||||
isGrouped: groupBy !== 'none',
|
||||
groupDepth: folderWorkspaceRow.groupDepth
|
||||
})
|
||||
const insetContentIndent = Math.max(0, contentIndent - surfaceInset)
|
||||
const { surfaceInset, cardContentIndent } = getFolderWorkspaceRowGeometry({
|
||||
experimentalNewWorktreeCardStyle: newCardStyle,
|
||||
isFolderBackedWorkspaceChild,
|
||||
isGrouped: groupBy !== 'none',
|
||||
groupDepth: folderWorkspaceRow.groupDepth,
|
||||
lineageDepth: folderWorkspaceRow.depth
|
||||
})
|
||||
return (
|
||||
<div
|
||||
key={vItem.key}
|
||||
@@ -4815,7 +4815,7 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
||||
repo={undefined}
|
||||
isActive={activeWorktreeId === folderWorktree.id}
|
||||
isCurrentWorktree={currentWorktreeId === folderWorktree.id}
|
||||
contentIndent={insetContentIndent}
|
||||
contentIndent={cardContentIndent}
|
||||
flushSurface
|
||||
nativeDragEnabled={false}
|
||||
onImmediateActivate={
|
||||
@@ -4826,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} />
|
||||
@@ -4910,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)
|
||||
@@ -5017,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)
|
||||
@@ -6417,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
|
||||
}
|
||||
@@ -36,4 +36,30 @@ describe('worktree card property options', () => {
|
||||
expect(options.map((option) => option.label)).toContain('Linear issues')
|
||||
expect(options.map((option) => option.label)).toContain('Automation')
|
||||
})
|
||||
|
||||
it('uses branch-only copy by default and without project groups', () => {
|
||||
const defaultOptions = getWorktreeCardPropertyOptions()
|
||||
const newCardOptions = getWorktreeCardPropertyOptions({
|
||||
newCardStyle: true,
|
||||
hasProjectGroups: false
|
||||
})
|
||||
|
||||
expect(defaultOptions.find((option) => option.id === 'branch')?.label).toBe('Branch name')
|
||||
expect(newCardOptions.find((option) => option.id === 'branch')?.label).toBe('Branch name')
|
||||
})
|
||||
|
||||
it('keeps branch-only copy for legacy cards even with project groups', () => {
|
||||
const options = getWorktreeCardPropertyOptions({ hasProjectGroups: true })
|
||||
|
||||
expect(options.find((option) => option.id === 'branch')?.label).toBe('Branch name')
|
||||
})
|
||||
|
||||
it('mentions folder paths only for new card style with project groups', () => {
|
||||
const options = getWorktreeCardPropertyOptions({
|
||||
newCardStyle: true,
|
||||
hasProjectGroups: true
|
||||
})
|
||||
|
||||
expect(options.find((option) => option.id === 'branch')?.label).toBe('Branch / folder path')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -158,21 +158,38 @@ const ISSUE_WORKTREE_CARD_PROPERTY_OPTIONS: WorktreeCardPropertyOption[] = [
|
||||
|
||||
type WorktreeCardPropertyOptionsInput = {
|
||||
newCardStyle?: boolean
|
||||
hasProjectGroups?: boolean
|
||||
}
|
||||
|
||||
export function getWorktreeCardPropertyOptions({
|
||||
newCardStyle = false
|
||||
newCardStyle = false,
|
||||
hasProjectGroups = false
|
||||
}: WorktreeCardPropertyOptionsInput = {}): WorktreeCardPropertyOption[] {
|
||||
const issueOptions = newCardStyle
|
||||
? ISSUE_WORKTREE_CARD_PROPERTY_OPTIONS
|
||||
: [TASK_WORKTREE_CARD_PROPERTY_OPTION]
|
||||
const branchOption: WorktreeCardPropertyOption = {
|
||||
id: 'branch',
|
||||
properties: ['branch'],
|
||||
get label() {
|
||||
// Why: new-card project groups can contain folder workspaces, so the
|
||||
// branch setting copy must describe both repo and folder identity.
|
||||
return newCardStyle && hasProjectGroups
|
||||
? translate(
|
||||
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.folderPathIdentity',
|
||||
'Branch / folder path'
|
||||
)
|
||||
: translate('auto.components.sidebar.SidebarWorkspaceOptionsMenu.219ebf1961', 'Branch name')
|
||||
}
|
||||
}
|
||||
if (newCardStyle) {
|
||||
return [...issueOptions, ...BASE_WORKTREE_CARD_PROPERTY_OPTIONS.slice(1)]
|
||||
return [...issueOptions, ...BASE_WORKTREE_CARD_PROPERTY_OPTIONS.slice(1, -1), branchOption]
|
||||
}
|
||||
return [
|
||||
BASE_WORKTREE_CARD_PROPERTY_OPTIONS[0],
|
||||
...issueOptions,
|
||||
...BASE_WORKTREE_CARD_PROPERTY_OPTIONS.slice(1)
|
||||
...BASE_WORKTREE_CARD_PROPERTY_OPTIONS.slice(1, -1),
|
||||
branchOption
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
getFolderBackedRepoWorktreeCardSurfaceInset,
|
||||
getFolderWorkspaceCardContentIndent,
|
||||
getFolderWorkspaceCardSurfaceInset,
|
||||
getFolderWorkspaceRowGeometry,
|
||||
getFlushWorktreeCardPaddingLeft,
|
||||
getLineageChildrenInlineStyle,
|
||||
getLineageEffectiveChildStart,
|
||||
@@ -21,6 +22,17 @@ import {
|
||||
getWorktreeCardSurfaceInset
|
||||
} from './worktree-list-indentation'
|
||||
|
||||
function getFlushCardContentStart(args: {
|
||||
surfaceInset: number
|
||||
cardContentIndent: number
|
||||
}): number {
|
||||
return (
|
||||
args.surfaceInset +
|
||||
WORKTREE_CARD_SURFACE_MARGIN +
|
||||
Math.max(FLUSH_CARD_MIN_CONTENT_INSET, args.cardContentIndent - FLUSH_CARD_CONTENT_PULLBACK)
|
||||
)
|
||||
}
|
||||
|
||||
describe('worktree list indentation', () => {
|
||||
it('keeps ungrouped workspaces flush with the list', () => {
|
||||
expect(getWorktreeCardContentIndent({ isGrouped: false, groupDepth: 4, lineageDepth: 0 })).toBe(
|
||||
@@ -78,6 +90,118 @@ describe('worktree list indentation', () => {
|
||||
expect(getFolderWorkspaceCardSurfaceInset({ isGrouped: false, groupDepth: 2 })).toBe(0)
|
||||
})
|
||||
|
||||
it('preserves legacy folder-scanned folder workspace row geometry', () => {
|
||||
const geometry = getFolderWorkspaceRowGeometry({
|
||||
experimentalNewWorktreeCardStyle: false,
|
||||
isFolderBackedWorkspaceChild: true,
|
||||
isGrouped: true,
|
||||
groupDepth: 1,
|
||||
lineageDepth: 0
|
||||
})
|
||||
|
||||
expect(geometry).toEqual({
|
||||
surfaceInset: 14,
|
||||
cardContentIndent: 6
|
||||
})
|
||||
expect(getFlushCardContentStart(geometry)).toBe(20)
|
||||
})
|
||||
|
||||
it('preserves legacy nested folder-scanned folder workspace row geometry', () => {
|
||||
const geometry = getFolderWorkspaceRowGeometry({
|
||||
experimentalNewWorktreeCardStyle: false,
|
||||
isFolderBackedWorkspaceChild: true,
|
||||
isGrouped: true,
|
||||
groupDepth: 2,
|
||||
lineageDepth: 0
|
||||
})
|
||||
|
||||
expect(geometry).toEqual({
|
||||
surfaceInset: 24,
|
||||
cardContentIndent: 6
|
||||
})
|
||||
expect(getFlushCardContentStart(geometry)).toBe(30)
|
||||
})
|
||||
|
||||
it('preserves legacy manual grouped folder workspace row geometry', () => {
|
||||
const geometry = getFolderWorkspaceRowGeometry({
|
||||
experimentalNewWorktreeCardStyle: false,
|
||||
isFolderBackedWorkspaceChild: false,
|
||||
isGrouped: true,
|
||||
groupDepth: 1,
|
||||
lineageDepth: 0
|
||||
})
|
||||
|
||||
expect(geometry).toEqual({
|
||||
surfaceInset: 14,
|
||||
cardContentIndent: 24
|
||||
})
|
||||
expect(getFlushCardContentStart(geometry)).toBe(38)
|
||||
})
|
||||
|
||||
it('uses comparable repo worktree geometry for experimental folder-scanned folder workspaces', () => {
|
||||
const geometry = getFolderWorkspaceRowGeometry({
|
||||
experimentalNewWorktreeCardStyle: true,
|
||||
isFolderBackedWorkspaceChild: true,
|
||||
isGrouped: true,
|
||||
groupDepth: 1,
|
||||
lineageDepth: 0
|
||||
})
|
||||
|
||||
expect(geometry).toEqual({
|
||||
surfaceInset: 14,
|
||||
cardContentIndent: 16
|
||||
})
|
||||
expect(getFlushCardContentStart(geometry)).toBe(30)
|
||||
})
|
||||
|
||||
it('uses comparable repo worktree geometry for experimental nested folder workspaces', () => {
|
||||
const geometry = getFolderWorkspaceRowGeometry({
|
||||
experimentalNewWorktreeCardStyle: true,
|
||||
isFolderBackedWorkspaceChild: true,
|
||||
isGrouped: true,
|
||||
groupDepth: 4,
|
||||
lineageDepth: 3
|
||||
})
|
||||
|
||||
expect(geometry).toEqual({
|
||||
surfaceInset: 54,
|
||||
cardContentIndent: 6
|
||||
})
|
||||
expect(getFlushCardContentStart(geometry)).toBe(60)
|
||||
})
|
||||
|
||||
it('keeps experimental manual grouped folder workspaces on normal worktree geometry', () => {
|
||||
const geometry = getFolderWorkspaceRowGeometry({
|
||||
experimentalNewWorktreeCardStyle: true,
|
||||
isFolderBackedWorkspaceChild: false,
|
||||
isGrouped: true,
|
||||
groupDepth: 1,
|
||||
lineageDepth: 0
|
||||
})
|
||||
|
||||
expect(geometry).toEqual({
|
||||
surfaceInset: 14,
|
||||
cardContentIndent: 24
|
||||
})
|
||||
expect(getFlushCardContentStart(geometry)).toBe(38)
|
||||
})
|
||||
|
||||
it('keeps experimental flat folder workspaces on normal worktree geometry', () => {
|
||||
const geometry = getFolderWorkspaceRowGeometry({
|
||||
experimentalNewWorktreeCardStyle: true,
|
||||
isFolderBackedWorkspaceChild: false,
|
||||
isGrouped: false,
|
||||
groupDepth: 3,
|
||||
lineageDepth: 0
|
||||
})
|
||||
|
||||
expect(geometry).toEqual({
|
||||
surfaceInset: 0,
|
||||
cardContentIndent: 0
|
||||
})
|
||||
expect(getFlushCardContentStart(geometry)).toBe(6)
|
||||
})
|
||||
|
||||
it('caps header indentation separately from workspace content indentation', () => {
|
||||
expect(getProjectGroupHeaderPaddingLeft(100)).toBe(70)
|
||||
})
|
||||
|
||||
@@ -102,6 +102,61 @@ export function getFolderWorkspaceCardSurfaceInset(args: {
|
||||
return Math.min(genericSurfaceInset, Math.max(0, maxSurfaceInset))
|
||||
}
|
||||
|
||||
export function getFolderWorkspaceRowGeometry(args: {
|
||||
experimentalNewWorktreeCardStyle: boolean
|
||||
isFolderBackedWorkspaceChild: boolean
|
||||
isGrouped: boolean
|
||||
groupDepth: number
|
||||
lineageDepth: number
|
||||
}): {
|
||||
surfaceInset: number
|
||||
cardContentIndent: number
|
||||
} {
|
||||
if (args.experimentalNewWorktreeCardStyle && args.isFolderBackedWorkspaceChild) {
|
||||
// Why: standalone folder workspace rows do not get a lineage wrapper
|
||||
// offset, so align them to the comparable folder-backed repo row anchor.
|
||||
const contentAnchor = getFolderBackedRepoWorktreeCardContentIndent({
|
||||
groupDepth: args.groupDepth,
|
||||
lineageDepth: 0
|
||||
})
|
||||
const surfaceInset = getFolderBackedRepoWorktreeCardSurfaceInset({
|
||||
groupDepth: args.groupDepth,
|
||||
lineageDepth: 0
|
||||
})
|
||||
|
||||
return {
|
||||
surfaceInset,
|
||||
cardContentIndent: Math.max(0, contentAnchor - surfaceInset)
|
||||
}
|
||||
}
|
||||
|
||||
const contentIndent = args.isFolderBackedWorkspaceChild
|
||||
? getFolderWorkspaceCardContentIndent({
|
||||
groupDepth: args.groupDepth
|
||||
})
|
||||
: getWorktreeCardContentIndent({
|
||||
isGrouped: args.isGrouped,
|
||||
groupDepth: args.groupDepth,
|
||||
lineageDepth: args.lineageDepth
|
||||
})
|
||||
// Why: legacy folder-scanned folder workspaces keep their compact anchor,
|
||||
// while all other folder rows share the normal worktree row surface path.
|
||||
const surfaceInset = args.isFolderBackedWorkspaceChild
|
||||
? getFolderWorkspaceCardSurfaceInset({
|
||||
isGrouped: true,
|
||||
groupDepth: args.groupDepth
|
||||
})
|
||||
: getWorktreeCardSurfaceInset({
|
||||
isGrouped: args.isGrouped,
|
||||
groupDepth: args.groupDepth
|
||||
})
|
||||
|
||||
return {
|
||||
surfaceInset,
|
||||
cardContentIndent: Math.max(0, contentIndent - surfaceInset)
|
||||
}
|
||||
}
|
||||
|
||||
export function getWorktreeCardSurfaceInset(args: {
|
||||
isGrouped: boolean
|
||||
groupDepth: number
|
||||
|
||||
@@ -3779,7 +3779,8 @@
|
||||
"8d62c68b35": "Notes",
|
||||
"2d74665a56": "Ports",
|
||||
"65a9820bd1": "Agent statuses",
|
||||
"219ebf1961": "Branch name"
|
||||
"219ebf1961": "Branch name",
|
||||
"folderPathIdentity": "Branch / folder path"
|
||||
},
|
||||
"SshDisconnectedDialog": {
|
||||
"ca4a7892af": "Connecting...",
|
||||
@@ -3855,7 +3856,9 @@
|
||||
"021538e1d1": "SSH disconnected",
|
||||
"runtimeHostDisconnected": "Server disconnected",
|
||||
"runtimeHostProject": "Project on Orca server",
|
||||
"automationCreated": "Created by automation"
|
||||
"automationCreated": "Created by automation",
|
||||
"branchIdentity": "Branch",
|
||||
"branchFolderPathIdentity": "Branch or folder path"
|
||||
},
|
||||
"WorktreeCardAgents": {
|
||||
"1b0a156717": "Agents"
|
||||
@@ -4301,6 +4304,9 @@
|
||||
"current": "Current",
|
||||
"searchPlaceholder": "Search worktrees...",
|
||||
"empty": "No matching eligible worktrees."
|
||||
},
|
||||
"WorktreeCardStatusSlot": {
|
||||
"branchIdentity": "Branch"
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
|
||||
@@ -3765,7 +3765,8 @@
|
||||
"automation": "Automation",
|
||||
"2d74665a56": "Puertos",
|
||||
"65a9820bd1": "Estados del agente",
|
||||
"219ebf1961": "Nombre de rama"
|
||||
"219ebf1961": "Nombre de rama",
|
||||
"folderPathIdentity": "Branch / folder path"
|
||||
},
|
||||
"SshDisconnectedDialog": {
|
||||
"ca4a7892af": "Conectando...",
|
||||
@@ -3841,7 +3842,9 @@
|
||||
"021538e1d1": "SSH desconectado",
|
||||
"runtimeHostDisconnected": "Server disconnected",
|
||||
"runtimeHostProject": "Project on Orca server",
|
||||
"automationCreated": "Created by automation"
|
||||
"automationCreated": "Created by automation",
|
||||
"branchIdentity": "Branch",
|
||||
"branchFolderPathIdentity": "Branch or folder path"
|
||||
},
|
||||
"WorktreeCardAgents": {
|
||||
"1b0a156717": "Agents"
|
||||
@@ -4301,6 +4304,9 @@
|
||||
"current": "Actual",
|
||||
"searchPlaceholder": "Buscar árboles de trabajo...",
|
||||
"empty": "No hay árboles de trabajo elegibles que coincidan."
|
||||
},
|
||||
"WorktreeCardStatusSlot": {
|
||||
"branchIdentity": "Branch"
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
|
||||
@@ -3746,7 +3746,8 @@
|
||||
"2d74665a56": "ポート",
|
||||
"65a9820bd1": "Agent ステータス",
|
||||
"219ebf1961": "ブランチ名",
|
||||
"automation": "Automation"
|
||||
"automation": "Automation",
|
||||
"folderPathIdentity": "Branch / folder path"
|
||||
},
|
||||
"SshDisconnectedDialog": {
|
||||
"ca4a7892af": "接続中...",
|
||||
@@ -3822,7 +3823,9 @@
|
||||
"021538e1d1": "SSH が切断されました",
|
||||
"runtimeHostDisconnected": "Server disconnected",
|
||||
"runtimeHostProject": "Project on Orca server",
|
||||
"automationCreated": "Created by automation"
|
||||
"automationCreated": "Created by automation",
|
||||
"branchIdentity": "Branch",
|
||||
"branchFolderPathIdentity": "Branch or folder path"
|
||||
},
|
||||
"WorktreeCardAgents": {
|
||||
"1b0a156717": "Agents"
|
||||
@@ -4301,6 +4304,9 @@
|
||||
"current": "現在",
|
||||
"searchPlaceholder": "ワークツリーを検索...",
|
||||
"empty": "一致する対象ワークツリーがありません。"
|
||||
},
|
||||
"WorktreeCardStatusSlot": {
|
||||
"branchIdentity": "Branch"
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
|
||||
@@ -3746,7 +3746,8 @@
|
||||
"2d74665a56": "포트",
|
||||
"65a9820bd1": "Agent 상태",
|
||||
"219ebf1961": "브랜치 이름",
|
||||
"automation": "자동화"
|
||||
"automation": "자동화",
|
||||
"folderPathIdentity": "Branch / folder path"
|
||||
},
|
||||
"SshDisconnectedDialog": {
|
||||
"ca4a7892af": "연결 중...",
|
||||
@@ -3822,7 +3823,9 @@
|
||||
"021538e1d1": "SSH 연결이 끊어졌습니다.",
|
||||
"runtimeHostDisconnected": "Server disconnected",
|
||||
"runtimeHostProject": "Project on Orca server",
|
||||
"automationCreated": "Created by automation"
|
||||
"automationCreated": "Created by automation",
|
||||
"branchIdentity": "Branch",
|
||||
"branchFolderPathIdentity": "Branch or folder path"
|
||||
},
|
||||
"WorktreeCardAgents": {
|
||||
"1b0a156717": "Agents"
|
||||
@@ -4301,6 +4304,9 @@
|
||||
"current": "현재",
|
||||
"searchPlaceholder": "워크트리 검색...",
|
||||
"empty": "일치하는 적격 워크트리가 없습니다."
|
||||
},
|
||||
"WorktreeCardStatusSlot": {
|
||||
"branchIdentity": "Branch"
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
|
||||
@@ -1102,7 +1102,8 @@
|
||||
"importHostSetup": "导入",
|
||||
"notePasteTooLarge": "笔记字段的粘贴内容过大。",
|
||||
"reuseExistingBranch": "复用分支",
|
||||
"reuseExistingBranchHint": "检出已有分支,而不是基于它创建新分支。"
|
||||
"reuseExistingBranchHint": "检出已有分支,而不是基于它创建新分支。",
|
||||
"createMultiple": "Create more"
|
||||
},
|
||||
"NewWorkspaceComposerModal": {
|
||||
"fa90f739a5": "创建工作区之前选择项目、工作区名称和 Agent。"
|
||||
@@ -3745,7 +3746,8 @@
|
||||
"2d74665a56": "端口",
|
||||
"65a9820bd1": "Agent 状态",
|
||||
"219ebf1961": "分支名称",
|
||||
"automation": "Automation"
|
||||
"automation": "Automation",
|
||||
"folderPathIdentity": "Branch / folder path"
|
||||
},
|
||||
"SshDisconnectedDialog": {
|
||||
"ca4a7892af": "正在连接...",
|
||||
@@ -3821,7 +3823,9 @@
|
||||
"021538e1d1": "SSH 已断开连接",
|
||||
"runtimeHostDisconnected": "Server disconnected",
|
||||
"runtimeHostProject": "Project on Orca server",
|
||||
"automationCreated": "Created by automation"
|
||||
"automationCreated": "Created by automation",
|
||||
"branchIdentity": "Branch",
|
||||
"branchFolderPathIdentity": "Branch or folder path"
|
||||
},
|
||||
"WorktreeCardAgents": {
|
||||
"1b0a156717": "Agents"
|
||||
@@ -4300,6 +4304,9 @@
|
||||
"current": "当前",
|
||||
"searchPlaceholder": "搜索工作树...",
|
||||
"empty": "没有匹配的可用工作树。"
|
||||
},
|
||||
"WorktreeCardStatusSlot": {
|
||||
"branchIdentity": "Branch"
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
|
||||
Reference in New Issue
Block a user