mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
Compact imported worktrees visibility prompt
This commit is contained in:
@@ -1,117 +0,0 @@
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import type { ComponentProps } from 'react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import ImportedWorktreesVisibilityCard from './ImportedWorktreesVisibilityCard'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
|
||||
const hiddenWorktrees = [
|
||||
{
|
||||
id: 'hidden-1',
|
||||
displayName: 'payments-refactor',
|
||||
path: '/worktrees/demo-project/payments-refactor',
|
||||
branch: 'refs/heads/payments-refactor'
|
||||
},
|
||||
{
|
||||
id: 'hidden-2',
|
||||
displayName: 'auth-cache-debug',
|
||||
path: '/worktrees/demo-project/auth-cache-debug',
|
||||
branch: 'refs/heads/auth-cache-debug'
|
||||
},
|
||||
{
|
||||
id: 'hidden-3',
|
||||
displayName: 'legacy-oauth-fix',
|
||||
path: '/worktrees/legacy/legacy-oauth-fix',
|
||||
branch: 'refs/heads/legacy-oauth-fix'
|
||||
},
|
||||
{
|
||||
id: 'hidden-4',
|
||||
displayName: 'ssh-worktree',
|
||||
path: '/srv/repos/orca/ssh-worktree',
|
||||
branch: 'refs/heads/ssh-worktree'
|
||||
}
|
||||
]
|
||||
|
||||
function renderCard(
|
||||
overrides: Partial<ComponentProps<typeof ImportedWorktreesVisibilityCard>> = {}
|
||||
): string {
|
||||
return renderToStaticMarkup(
|
||||
<TooltipProvider>
|
||||
<ImportedWorktreesVisibilityCard
|
||||
repoDisplayName="orca"
|
||||
hiddenWorktrees={hiddenWorktrees}
|
||||
placement="repo-group"
|
||||
pending={false}
|
||||
error={null}
|
||||
onShow={vi.fn()}
|
||||
onKeepHidden={vi.fn()}
|
||||
{...overrides}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
describe('ImportedWorktreesVisibilityCard', () => {
|
||||
it('renders the required repo-group copy, three-item preview, actions, and repo menu hint', () => {
|
||||
const markup = renderCard()
|
||||
|
||||
expect(markup).toContain('Imported 4 existing worktrees')
|
||||
expect(markup).toContain(
|
||||
'Orca found 4 worktrees and imported them automatically into this repo.'
|
||||
)
|
||||
expect(markup).toContain('payments-refactor')
|
||||
expect(markup).toContain('auth-cache-debug')
|
||||
expect(markup).toContain('legacy-oauth-fix')
|
||||
expect(markup).toContain('/worktrees/demo-project')
|
||||
expect(markup).toContain('/worktrees/legacy')
|
||||
expect((markup.match(/>hidden</g) ?? []).length).toBe(3)
|
||||
expect(markup).toContain('Show 1 more')
|
||||
expect(markup).not.toContain('ssh-worktree')
|
||||
expect(markup).not.toContain('refs/heads/payments-refactor')
|
||||
expect(markup).not.toContain('/worktrees/demo-project/payments-refactor')
|
||||
expect(markup).toContain('repo options')
|
||||
expect(markup).toContain('Keep hidden')
|
||||
expect(markup).toContain('Show')
|
||||
})
|
||||
|
||||
it('scopes pinned fallback copy to the repo name', () => {
|
||||
const markup = renderCard({ placement: 'pinned-fallback' })
|
||||
|
||||
expect(markup).toContain('Imported 4 existing worktrees in orca')
|
||||
expect(markup).toContain('imported them automatically into orca.')
|
||||
expect(markup).toContain('Showing them restores the imported worktrees to the repo list.')
|
||||
expect(markup).not.toContain('repo options')
|
||||
})
|
||||
|
||||
it('preserves Windows parent path separators in the preview', () => {
|
||||
const markup = renderCard({
|
||||
hiddenWorktrees: [
|
||||
{
|
||||
id: 'windows-hidden',
|
||||
displayName: 'FeatureX',
|
||||
path: 'C:\\Repos\\Orca\\FeatureX'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
expect(markup).toContain('C:\\Repos\\Orca')
|
||||
expect(markup).not.toContain('C:/Repos/Orca')
|
||||
})
|
||||
|
||||
it('does not expose Keep hidden in the pinned-only fallback state', () => {
|
||||
const markup = renderCard({ placement: 'pinned-fallback', onKeepHidden: undefined })
|
||||
|
||||
expect(markup).not.toContain('Keep hidden')
|
||||
expect(markup).toContain('Use Show to restore this repo')
|
||||
expect(markup).toContain('Show')
|
||||
})
|
||||
|
||||
it('disables actions while pending and renders inline errors', () => {
|
||||
const markup = renderCard({ pending: true, error: 'Could not show imported worktrees.' })
|
||||
|
||||
expect(markup).toContain('aria-busy="true"')
|
||||
expect(markup).toContain('disabled=""')
|
||||
expect(markup).toContain('role="alert"')
|
||||
expect(markup).toContain('Could not show imported worktrees.')
|
||||
})
|
||||
})
|
||||
@@ -1,223 +0,0 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Ellipsis } from 'lucide-react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { dirname } from '@/lib/path'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export type ImportedWorktreesVisibilityPlacement = 'repo-group' | 'pinned-fallback'
|
||||
|
||||
export type ImportedWorktreeVisibilityPreview = {
|
||||
id?: string
|
||||
displayName: string
|
||||
path?: string
|
||||
branch?: string
|
||||
}
|
||||
|
||||
type ImportedWorktreesVisibilityCardProps = {
|
||||
repoDisplayName: string
|
||||
hiddenWorktrees: readonly ImportedWorktreeVisibilityPreview[]
|
||||
placement: ImportedWorktreesVisibilityPlacement
|
||||
pending: boolean
|
||||
error: string | null
|
||||
onShow: () => void
|
||||
onKeepHidden?: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const PREVIEW_LIMIT = 3
|
||||
const UNKNOWN_LOCATION_LABEL = 'Unknown location'
|
||||
|
||||
type ImportedWorktreePathGroup = {
|
||||
path: string
|
||||
worktrees: ImportedWorktreeVisibilityPreview[]
|
||||
}
|
||||
|
||||
function pluralizeWorktree(count: number): string {
|
||||
return count === 1 ? 'worktree' : 'worktrees'
|
||||
}
|
||||
|
||||
function getWorktreeKey(
|
||||
worktree: ImportedWorktreeVisibilityPreview,
|
||||
index: number,
|
||||
prefix: string
|
||||
): string {
|
||||
return worktree.id ?? worktree.path ?? `${prefix}-${worktree.displayName}-${index}`
|
||||
}
|
||||
|
||||
function getParentPath(path: string | undefined): string {
|
||||
if (!path) {
|
||||
return UNKNOWN_LOCATION_LABEL
|
||||
}
|
||||
const parentPath = dirname(path)
|
||||
if (!parentPath || parentPath === '.') {
|
||||
return UNKNOWN_LOCATION_LABEL
|
||||
}
|
||||
return parentPath
|
||||
}
|
||||
|
||||
function groupWorktreesByParentPath(
|
||||
worktrees: readonly ImportedWorktreeVisibilityPreview[]
|
||||
): ImportedWorktreePathGroup[] {
|
||||
const groups: ImportedWorktreePathGroup[] = []
|
||||
const groupByPath = new Map<string, ImportedWorktreePathGroup>()
|
||||
for (const worktree of worktrees) {
|
||||
const path = getParentPath(worktree.path)
|
||||
const existing = groupByPath.get(path)
|
||||
if (existing) {
|
||||
existing.worktrees.push(worktree)
|
||||
continue
|
||||
}
|
||||
const group = { path, worktrees: [worktree] }
|
||||
groupByPath.set(path, group)
|
||||
groups.push(group)
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
export default function ImportedWorktreesVisibilityCard({
|
||||
repoDisplayName,
|
||||
hiddenWorktrees,
|
||||
placement,
|
||||
pending,
|
||||
error,
|
||||
onShow,
|
||||
onKeepHidden,
|
||||
className
|
||||
}: ImportedWorktreesVisibilityCardProps): React.JSX.Element | null {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const hiddenCount = hiddenWorktrees.length
|
||||
const worktreeNoun = pluralizeWorktree(hiddenCount)
|
||||
const visibleWorktrees = isExpanded ? hiddenWorktrees : hiddenWorktrees.slice(0, PREVIEW_LIMIT)
|
||||
const visibleWorktreeGroups = groupWorktreesByParentPath(visibleWorktrees)
|
||||
const remainingCount = Math.max(0, hiddenWorktrees.length - visibleWorktrees.length)
|
||||
|
||||
if (hiddenCount === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const title =
|
||||
placement === 'pinned-fallback'
|
||||
? `Imported ${hiddenCount} existing ${worktreeNoun} in ${repoDisplayName}`
|
||||
: `Imported ${hiddenCount} existing ${worktreeNoun}`
|
||||
const subtitle =
|
||||
placement === 'pinned-fallback'
|
||||
? `Orca found ${hiddenCount} ${worktreeNoun} and imported them automatically into ${repoDisplayName}.`
|
||||
: `Orca found ${hiddenCount} ${worktreeNoun} and imported them automatically into this repo.`
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-busy={pending}
|
||||
className={cn(
|
||||
'mx-1 my-1.5 rounded-lg border border-sidebar-border bg-sidebar-accent/60 p-2.5 text-sidebar-foreground',
|
||||
placement === 'repo-group' ? 'ml-9' : 'ml-7',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="truncate text-[13px] font-semibold leading-5">{title}</h3>
|
||||
<p className="mt-1 text-[11px] leading-4 text-muted-foreground">{subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 grid gap-1.5" aria-label="Imported worktree preview">
|
||||
{visibleWorktreeGroups.map((group) => (
|
||||
<div key={group.path} className="grid min-w-0 gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
tabIndex={0}
|
||||
className="block w-full min-w-0 truncate px-1 font-mono text-[10px] leading-4 text-muted-foreground outline-none focus-visible:ring-1 focus-visible:ring-sidebar-ring"
|
||||
>
|
||||
{group.path}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={4}>
|
||||
{group.path}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{group.worktrees.map((worktree, index) => (
|
||||
<div
|
||||
key={getWorktreeKey(worktree, index, 'preview')}
|
||||
className="flex min-h-6 min-w-0 items-center justify-between gap-2 rounded-md bg-sidebar px-2 text-xs"
|
||||
>
|
||||
<span className="min-w-0 truncate font-medium text-sidebar-foreground">
|
||||
{worktree.displayName}
|
||||
</span>
|
||||
<span className="shrink-0 text-[11px] text-muted-foreground">hidden</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{remainingCount > 0 ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
disabled={pending}
|
||||
onClick={() => setIsExpanded(true)}
|
||||
className="mt-1.5 h-6 px-2 text-[11px] text-muted-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||
>
|
||||
Show {remainingCount} more
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{placement === 'repo-group' ? (
|
||||
<p className="mt-2 text-[11px] leading-4 text-muted-foreground">
|
||||
They are currently hidden, but you can show or hide them anytime by clicking{' '}
|
||||
<span className="inline-flex size-5 align-middle items-center justify-center rounded-md border border-sidebar-border bg-sidebar-accent text-muted-foreground">
|
||||
<Ellipsis className="size-3" aria-hidden="true" />
|
||||
<span className="sr-only">repo options</span>
|
||||
</span>{' '}
|
||||
on this repo.
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-2 text-[11px] leading-4 text-muted-foreground">
|
||||
They are currently hidden in this view. Showing them restores the imported worktrees to
|
||||
the repo list.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{error ? (
|
||||
<p className="mt-2 text-[11px] leading-4 text-destructive" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="mt-2.5 flex items-center justify-between gap-2 border-t border-sidebar-border pt-2">
|
||||
{onKeepHidden ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
disabled={pending}
|
||||
aria-label={`Keep ${hiddenCount} imported ${worktreeNoun} hidden for ${repoDisplayName}`}
|
||||
onClick={onKeepHidden}
|
||||
className="text-muted-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||
>
|
||||
Keep hidden
|
||||
</Button>
|
||||
) : (
|
||||
<span className="min-w-0 text-[11px] leading-4 text-muted-foreground">
|
||||
Use Show to restore this repo's imported worktrees.
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
size="xs"
|
||||
disabled={pending}
|
||||
aria-label={`Show ${hiddenCount} imported ${worktreeNoun} for ${repoDisplayName}`}
|
||||
onClick={onShow}
|
||||
>
|
||||
Show
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export type { ImportedWorktreesVisibilityCardProps }
|
||||
@@ -0,0 +1,103 @@
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import type { ComponentProps } from 'react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import ImportedWorktreesVisibilityLine, {
|
||||
groupWorktreesByParentPath
|
||||
} from './ImportedWorktreesVisibilityLine'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
|
||||
const hiddenWorktrees = [
|
||||
{
|
||||
id: 'hidden-1',
|
||||
displayName: 'payments-refactor',
|
||||
path: '/worktrees/demo-project/payments-refactor',
|
||||
branch: 'refs/heads/payments-refactor'
|
||||
},
|
||||
{
|
||||
id: 'hidden-2',
|
||||
displayName: 'auth-cache-debug',
|
||||
path: '/worktrees/demo-project/auth-cache-debug',
|
||||
branch: 'refs/heads/auth-cache-debug'
|
||||
},
|
||||
{
|
||||
id: 'hidden-3',
|
||||
displayName: 'legacy-oauth-fix',
|
||||
path: '/worktrees/legacy/legacy-oauth-fix',
|
||||
branch: 'refs/heads/legacy-oauth-fix'
|
||||
},
|
||||
{
|
||||
id: 'hidden-4',
|
||||
displayName: 'ssh-worktree',
|
||||
path: '/srv/repos/orca/ssh-worktree',
|
||||
branch: 'refs/heads/ssh-worktree'
|
||||
}
|
||||
]
|
||||
|
||||
function renderLine(
|
||||
overrides: Partial<ComponentProps<typeof ImportedWorktreesVisibilityLine>> = {}
|
||||
): string {
|
||||
return renderToStaticMarkup(
|
||||
<TooltipProvider>
|
||||
<ImportedWorktreesVisibilityLine
|
||||
repoDisplayName="orca"
|
||||
hiddenWorktrees={hiddenWorktrees}
|
||||
placement="repo-group"
|
||||
pending={false}
|
||||
error={null}
|
||||
onShow={vi.fn()}
|
||||
onKeepHidden={vi.fn()}
|
||||
{...overrides}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
describe('ImportedWorktreesVisibilityLine', () => {
|
||||
it('renders the compact repo-group line with inline show and dismiss actions', () => {
|
||||
const markup = renderLine()
|
||||
|
||||
expect(markup).toContain('Hiding 4 discovered worktrees')
|
||||
expect(markup).toContain('Show all')
|
||||
expect(markup).toContain('Show all 4 discovered worktrees for orca')
|
||||
expect(markup).toContain(
|
||||
'Keep 4 discovered worktrees hidden for orca; recover from the repo menu'
|
||||
)
|
||||
expect(markup).toContain('aria-expanded="false"')
|
||||
expect(markup).not.toContain('Imported 4 existing worktrees')
|
||||
expect(markup).not.toContain('Orca found 4 worktrees')
|
||||
expect(markup).not.toContain('repo options')
|
||||
expect(markup).not.toContain('payments-refactor')
|
||||
expect(markup).not.toContain('/worktrees/demo-project')
|
||||
})
|
||||
|
||||
it('scopes pinned fallback copy to the repo name without a dismiss action', () => {
|
||||
const markup = renderLine({ placement: 'pinned-fallback', onKeepHidden: undefined })
|
||||
|
||||
expect(markup).toContain('Hiding 4 discovered worktrees in orca')
|
||||
expect(markup).toContain('Show all')
|
||||
expect(markup).not.toContain('Keep hidden - recover from the repo menu')
|
||||
})
|
||||
|
||||
it('preserves Windows parent path separators in preview groups', () => {
|
||||
const groups = groupWorktreesByParentPath([
|
||||
{
|
||||
id: 'windows-hidden',
|
||||
displayName: 'FeatureX',
|
||||
path: 'C:\\Repos\\Orca\\FeatureX'
|
||||
}
|
||||
])
|
||||
|
||||
expect(groups).toMatchObject([{ path: 'C:\\Repos\\Orca' }])
|
||||
expect(groups[0]?.path).not.toBe('C:/Repos/Orca')
|
||||
})
|
||||
|
||||
it('disables actions while pending and renders inline errors', () => {
|
||||
const markup = renderLine({ pending: true, error: 'Could not show imported worktrees.' })
|
||||
|
||||
expect(markup).toContain('aria-busy="true"')
|
||||
expect(markup).toContain('disabled=""')
|
||||
expect(markup).toContain('role="alert"')
|
||||
expect(markup).toContain('Could not show imported worktrees.')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,216 @@
|
||||
import React, { useState } from 'react'
|
||||
import { ChevronRight, EyeOff, X } from 'lucide-react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { dirname } from '@/lib/path'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export type ImportedWorktreesVisibilityPlacement = 'repo-group' | 'pinned-fallback'
|
||||
|
||||
export type ImportedWorktreeVisibilityPreview = {
|
||||
id?: string
|
||||
displayName: string
|
||||
path?: string
|
||||
branch?: string
|
||||
}
|
||||
|
||||
type ImportedWorktreesVisibilityLineProps = {
|
||||
repoDisplayName: string
|
||||
hiddenWorktrees: readonly ImportedWorktreeVisibilityPreview[]
|
||||
placement: ImportedWorktreesVisibilityPlacement
|
||||
pending: boolean
|
||||
error: string | null
|
||||
onShow: () => void
|
||||
onKeepHidden?: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const PREVIEW_LIMIT = 3
|
||||
const UNKNOWN_LOCATION_LABEL = 'Unknown location'
|
||||
const KEEP_HIDDEN_LABEL = 'Keep hidden - recover from the repo menu'
|
||||
|
||||
type ImportedWorktreePathGroup = {
|
||||
path: string
|
||||
worktrees: ImportedWorktreeVisibilityPreview[]
|
||||
}
|
||||
|
||||
function pluralizeWorktree(count: number): string {
|
||||
return count === 1 ? 'worktree' : 'worktrees'
|
||||
}
|
||||
|
||||
function getWorktreeKey(
|
||||
worktree: ImportedWorktreeVisibilityPreview,
|
||||
index: number,
|
||||
prefix: string
|
||||
): string {
|
||||
return worktree.id ?? worktree.path ?? `${prefix}-${worktree.displayName}-${index}`
|
||||
}
|
||||
|
||||
function getParentPath(path: string | undefined): string {
|
||||
if (!path) {
|
||||
return UNKNOWN_LOCATION_LABEL
|
||||
}
|
||||
const parentPath = dirname(path)
|
||||
if (!parentPath || parentPath === '.') {
|
||||
return UNKNOWN_LOCATION_LABEL
|
||||
}
|
||||
return parentPath
|
||||
}
|
||||
|
||||
export function groupWorktreesByParentPath(
|
||||
worktrees: readonly ImportedWorktreeVisibilityPreview[]
|
||||
): ImportedWorktreePathGroup[] {
|
||||
const groups: ImportedWorktreePathGroup[] = []
|
||||
const groupByPath = new Map<string, ImportedWorktreePathGroup>()
|
||||
for (const worktree of worktrees) {
|
||||
const path = getParentPath(worktree.path)
|
||||
const existing = groupByPath.get(path)
|
||||
if (existing) {
|
||||
existing.worktrees.push(worktree)
|
||||
continue
|
||||
}
|
||||
const group = { path, worktrees: [worktree] }
|
||||
groupByPath.set(path, group)
|
||||
groups.push(group)
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
export default function ImportedWorktreesVisibilityLine({
|
||||
repoDisplayName,
|
||||
hiddenWorktrees,
|
||||
placement,
|
||||
pending,
|
||||
error,
|
||||
onShow,
|
||||
onKeepHidden,
|
||||
className
|
||||
}: ImportedWorktreesVisibilityLineProps): React.JSX.Element | null {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const hiddenCount = hiddenWorktrees.length
|
||||
const worktreeNoun = pluralizeWorktree(hiddenCount)
|
||||
const visibleWorktrees = hiddenWorktrees.slice(0, PREVIEW_LIMIT)
|
||||
const visibleWorktreeGroups = groupWorktreesByParentPath(visibleWorktrees)
|
||||
const remainingCount = Math.max(0, hiddenWorktrees.length - visibleWorktrees.length)
|
||||
const keepHiddenAriaLabel = `Keep ${hiddenCount} discovered ${worktreeNoun} hidden for ${repoDisplayName}; recover from the repo menu`
|
||||
|
||||
if (hiddenCount === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const lineText =
|
||||
placement === 'pinned-fallback'
|
||||
? `Hiding ${hiddenCount} discovered ${worktreeNoun} in ${repoDisplayName}`
|
||||
: `Hiding ${hiddenCount} discovered ${worktreeNoun}`
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-busy={pending}
|
||||
className={cn('mx-1 my-0.5 ml-5 text-sidebar-foreground', className)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex min-h-7 min-w-0 items-center gap-1.5 rounded-md px-1.5 text-[11px] leading-none text-muted-foreground transition-colors',
|
||||
'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground'
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
disabled={pending}
|
||||
aria-expanded={isExpanded}
|
||||
aria-label={`${isExpanded ? 'Collapse' : 'Expand'} hidden worktrees for ${repoDisplayName}`}
|
||||
onClick={() => setIsExpanded((value) => !value)}
|
||||
className="shrink-0 rounded-[4px] text-muted-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn('size-3 transition-transform', isExpanded && 'rotate-90')}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Button>
|
||||
<EyeOff className="size-3 shrink-0" aria-hidden="true" />
|
||||
<span className="min-w-0 flex-1 truncate">{lineText}</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
disabled={pending}
|
||||
aria-label={`Show all ${hiddenCount} discovered ${worktreeNoun} for ${repoDisplayName}`}
|
||||
onClick={onShow}
|
||||
className="h-6 shrink-0 px-1.5 text-[11px] font-medium text-sidebar-primary hover:bg-sidebar-accent hover:text-sidebar-primary"
|
||||
>
|
||||
Show all
|
||||
</Button>
|
||||
{onKeepHidden ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
disabled={pending}
|
||||
aria-label={keepHiddenAriaLabel}
|
||||
onClick={onKeepHidden}
|
||||
className="shrink-0 rounded-md text-muted-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||
>
|
||||
<X className="size-3" aria-hidden="true" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={4}>
|
||||
{KEEP_HIDDEN_LABEL}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{isExpanded ? (
|
||||
<div className="mt-0.5 grid gap-0.5 pb-1" aria-label="Hidden worktree preview">
|
||||
{visibleWorktreeGroups.map((group) => (
|
||||
<div key={group.path} className="grid min-w-0 gap-0.5">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
tabIndex={0}
|
||||
className="block min-w-0 truncate py-1 pl-7 pr-2 font-mono text-[10px] leading-4 text-muted-foreground outline-none focus-visible:ring-1 focus-visible:ring-sidebar-ring"
|
||||
>
|
||||
{group.path}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={4}>
|
||||
{group.path}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{group.worktrees.map((worktree, index) => (
|
||||
<div
|
||||
key={getWorktreeKey(worktree, index, 'preview')}
|
||||
className="flex min-h-7 min-w-0 items-center gap-2 rounded-md py-0 pl-5 pr-2 text-xs text-muted-foreground hover:bg-sidebar-accent"
|
||||
>
|
||||
<span
|
||||
className="size-2 shrink-0 rounded-full border border-dashed border-muted-foreground/50"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="min-w-0 truncate font-medium">{worktree.displayName}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
{remainingCount > 0 ? (
|
||||
<div className="py-1 pl-7 pr-2 text-[11px] leading-4 text-muted-foreground">
|
||||
+ {remainingCount} more
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<p className="px-1.5 pb-1 pt-0.5 text-[11px] leading-4 text-destructive" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export type { ImportedWorktreesVisibilityLineProps }
|
||||
@@ -170,7 +170,7 @@ import {
|
||||
} from '../../../../shared/worktree-ownership'
|
||||
import { RepoIconGlyph } from '@/components/repo/repo-icon'
|
||||
import { RepoBadgeMark } from '@/components/repo/RepoBadgeLabel'
|
||||
import ImportedWorktreesVisibilityCard from './ImportedWorktreesVisibilityCard'
|
||||
import ImportedWorktreesVisibilityLine from './ImportedWorktreesVisibilityLine'
|
||||
import {
|
||||
keepImportedWorktreesHiddenCard,
|
||||
showImportedWorktreesCard,
|
||||
@@ -293,7 +293,7 @@ function getWorktreeVisibilityMenuLabel(repo: Repo): string {
|
||||
repo,
|
||||
isLegacyRepoForExternalWorktreeVisibility(repo)
|
||||
)
|
||||
return visibility === 'show' ? 'Hide non-Orca worktrees' : 'Import Worktrees'
|
||||
return visibility === 'show' ? 'Hide non-Orca worktrees' : 'Show hidden worktrees'
|
||||
}
|
||||
|
||||
const LINEAGE_INDENT = 18
|
||||
@@ -3016,7 +3016,7 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
||||
className="absolute left-0 right-0 top-0"
|
||||
style={{ transform: getVirtualRowTransform(vItem.start) }}
|
||||
>
|
||||
<ImportedWorktreesVisibilityCard
|
||||
<ImportedWorktreesVisibilityLine
|
||||
repoDisplayName={row.repo.displayName}
|
||||
hiddenWorktrees={row.hiddenWorktrees}
|
||||
placement={row.placement}
|
||||
|
||||
@@ -251,10 +251,10 @@ describe('estimateRenderRowSize', () => {
|
||||
expect(activeSize).toBe(36)
|
||||
})
|
||||
|
||||
it('estimates imported worktree card rows with a stable larger height', () => {
|
||||
it('estimates imported worktree line rows with a stable compact height', () => {
|
||||
const rows = [makeHeaderRow('repo:repo-1'), makeImportedCardRow()]
|
||||
|
||||
expect(estimateRenderRowSize(rows, 1, 0, null)).toBe(224)
|
||||
expect(estimateRenderRowSize(rows, 1, 0, null)).toBe(36)
|
||||
})
|
||||
|
||||
it('keeps the previous header active until the secondary header row reaches the top', () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { PINNED_GROUP_KEY } from './worktree-list-groups'
|
||||
|
||||
export const GROUP_HEADER_ROW_HEIGHT = 28
|
||||
const SECONDARY_GROUP_HEADER_TOP_MARGIN = 8
|
||||
const IMPORTED_WORKTREES_CARD_ROW_HEIGHT = 224
|
||||
const IMPORTED_WORKTREES_LINE_ROW_HEIGHT = 36
|
||||
|
||||
type WorktreeItemRow = Extract<Row, { type: 'item' }>
|
||||
export type RenderRow = Row | { type: 'lineage-group'; key: string; rows: WorktreeItemRow[] }
|
||||
@@ -44,7 +44,7 @@ export function estimateRenderRowSize(
|
||||
return 100 + Math.max(0, row.rows.length - 1) * 96
|
||||
}
|
||||
if (row?.type === 'imported-worktrees-card') {
|
||||
return IMPORTED_WORKTREES_CARD_ROW_HEIGHT
|
||||
return IMPORTED_WORKTREES_LINE_ROW_HEIGHT
|
||||
}
|
||||
return 116
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user