diff --git a/src/renderer/src/components/sidebar/ImportedWorktreesVisibilityLine.test.tsx b/src/renderer/src/components/sidebar/ImportedWorktreesVisibilityLine.test.tsx index 527c7496a41..0dc3f7c3ce2 100644 --- a/src/renderer/src/components/sidebar/ImportedWorktreesVisibilityLine.test.tsx +++ b/src/renderer/src/components/sidebar/ImportedWorktreesVisibilityLine.test.tsx @@ -73,6 +73,26 @@ describe('ImportedWorktreesVisibilityLine', () => { expect(markup).not.toContain('/worktrees/demo-project') }) + it('names the host when the project is checked out on more than one', () => { + const markup = renderLine({ hostContextLabel: 'openclaw' }) + + expect(markup).toContain('openclaw') + expect(markup).toContain('Expand hidden worktrees for orca on openclaw') + expect(markup).toContain( + 'Keep 4 discovered worktrees hidden for orca on openclaw; recover from the project menu' + ) + }) + + it('folds the host into pinned fallback copy, which already names the repo', () => { + const markup = renderLine({ + hostContextLabel: 'openclaw', + placement: 'pinned-fallback', + onKeepHidden: undefined + }) + + expect(markup).toContain('Hiding 4 discovered worktrees in orca on openclaw') + }) + it('scopes pinned fallback copy to the repo name without a dismiss action', () => { const markup = renderLine({ placement: 'pinned-fallback', onKeepHidden: undefined }) diff --git a/src/renderer/src/components/sidebar/ImportedWorktreesVisibilityLine.tsx b/src/renderer/src/components/sidebar/ImportedWorktreesVisibilityLine.tsx index 3c82858579e..60dcff4883d 100644 --- a/src/renderer/src/components/sidebar/ImportedWorktreesVisibilityLine.tsx +++ b/src/renderer/src/components/sidebar/ImportedWorktreesVisibilityLine.tsx @@ -19,6 +19,9 @@ export type ImportedWorktreeVisibilityPreview = { type ImportedWorktreesVisibilityLineProps = { repoDisplayName: string + /** Host this checkout lives on. Set only when the project is checked out on + * more than one host, where the line alone cannot identify the row. */ + hostContextLabel?: string hiddenWorktrees: readonly ImportedWorktreeVisibilityPreview[] placement: ImportedWorktreesVisibilityPlacement pending: boolean @@ -74,6 +77,7 @@ export function groupWorktreesByParentPath( export default function ImportedWorktreesVisibilityLine({ repoDisplayName, + hostContextLabel, hiddenWorktrees, placement, pending, @@ -89,7 +93,11 @@ export default function ImportedWorktreesVisibilityLine({ const worktreeGroups = groupWorktreesByParentPath(hiddenWorktrees) const visibleWorktreeGroups = worktreeGroups.slice(0, GROUP_LIMIT) const remainingGroupCount = Math.max(0, worktreeGroups.length - visibleWorktreeGroups.length) - const keepHiddenAriaLabel = `Keep ${hiddenCount} discovered ${worktreeNoun} hidden for ${repoDisplayName}; recover from the project menu` + // Why: two hosts checking out one project render two identical lines. + const repoScopeLabel = hostContextLabel + ? `${repoDisplayName} on ${hostContextLabel}` + : repoDisplayName + const keepHiddenAriaLabel = `Keep ${hiddenCount} discovered ${worktreeNoun} hidden for ${repoScopeLabel}; recover from the project menu` if (hiddenCount === 0) { return null @@ -97,7 +105,7 @@ export default function ImportedWorktreesVisibilityLine({ const lineText = placement === 'pinned-fallback' - ? `Hiding ${hiddenCount} discovered ${worktreeNoun} in ${repoDisplayName}` + ? `Hiding ${hiddenCount} discovered ${worktreeNoun} in ${repoScopeLabel}` : `Hiding ${hiddenCount} discovered ${worktreeNoun}` const toggleGroupExpanded = (path: string): void => { @@ -133,7 +141,7 @@ export default function ImportedWorktreesVisibilityLine({ aria-label={translate( 'auto.components.sidebar.ImportedWorktreesVisibilityLine.f54f2bec5d', '{{value0}} hidden worktrees for {{value1}}', - { value0: isExpanded ? 'Collapse' : 'Expand', value1: repoDisplayName } + { value0: isExpanded ? 'Collapse' : 'Expand', value1: repoScopeLabel } )} onClick={() => setIsExpanded((value) => !value)} className="shrink-0 rounded-[4px] text-muted-foreground hover:bg-worktree-sidebar-accent hover:text-worktree-sidebar-accent-foreground" @@ -144,6 +152,11 @@ export default function ImportedWorktreesVisibilityLine({ /> {lineText} + {hostContextLabel && placement !== 'pinned-fallback' ? ( + + {hostContextLabel} + + ) : null} {onKeepHidden ? ( diff --git a/src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.test.tsx b/src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.test.tsx index 38554945f12..426f9adf02d 100644 --- a/src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.test.tsx +++ b/src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.test.tsx @@ -17,6 +17,7 @@ vi.mock('@/components/ui/tooltip', () => ({ const roots: Root[] = [] type RenderOverrides = { + hostContextLabel?: string inboxCount?: number pending?: boolean error?: string | null @@ -34,6 +35,7 @@ async function renderLine(overrides: RenderOverrides = {}): Promise { ) }) + it('names the host so two checkouts of one project are distinguishable', async () => { + // Both rows read "N hidden worktrees"; only the host tells them apart. + const local = await renderLine({ hostContextLabel: 'Local Mac', inboxCount: 61 }) + const remote = await renderLine({ hostContextLabel: 'openclaw', inboxCount: 134 }) + + expect(local.textContent).toContain('Local Mac') + expect(getReviewButton(local)?.getAttribute('aria-label')).toBe( + 'Review 61 hidden worktrees in orca on Local Mac' + ) + expect(getReviewButton(remote)?.getAttribute('aria-label')).toBe( + 'Review 134 hidden worktrees in orca on openclaw' + ) + }) + + it('host-qualifies the suppress control, which writes to that host alone', async () => { + const container = await renderLine({ hostContextLabel: 'openclaw', onSuppress: vi.fn() }) + + expect( + container.querySelector( + 'button[aria-label="Hide external worktrees permanently for orca on openclaw"]' + ) + ).not.toBeNull() + }) + + it('stays unqualified when the project has a single checkout', async () => { + const container = await renderLine() + + expect(getReviewButton(container)?.getAttribute('aria-label')).toBe( + 'Review 24 hidden worktrees in orca' + ) + }) + it('keeps suppress as a hover-revealed control that does not trigger review', async () => { const onReview = vi.fn() const onSuppress = vi.fn() diff --git a/src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.tsx b/src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.tsx index 657b3b254e0..78a1999f93d 100644 --- a/src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.tsx +++ b/src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.tsx @@ -8,6 +8,9 @@ import { translate } from '@/i18n/i18n' type NewExternalWorktreesInboxLineProps = { repoDisplayName: string + /** Host this checkout lives on. Set only when the project is checked out on + * more than one host, where the count alone cannot identify the row. */ + hostContextLabel?: string inboxCount: number pending: boolean error: string | null @@ -18,6 +21,7 @@ type NewExternalWorktreesInboxLineProps = { export default function NewExternalWorktreesInboxLine({ repoDisplayName, + hostContextLabel, inboxCount, pending, error, @@ -29,10 +33,19 @@ export default function NewExternalWorktreesInboxLine({ 'auto.components.sidebar.NewExternalWorktreesInboxLine.c3e8a1f4b2', "Don't show again" ) + // Why: the same project on two hosts renders two identical rows, so every + // accessible name has to name the host as well as the project. + const repoScopeLabel = hostContextLabel + ? translate( + 'auto.components.sidebar.NewExternalWorktreesInboxLine.6c07f3a91e', + '{{value0}} on {{value1}}', + { value0: repoDisplayName, value1: hostContextLabel } + ) + : repoDisplayName const suppressAriaLabel = translate( 'auto.components.sidebar.NewExternalWorktreesInboxLine.9f2d4c8b17', 'Hide external worktrees permanently for {{value0}}', - { value0: repoDisplayName } + { value0: repoScopeLabel } ) const isSingular = inboxCount === 1 const countLabel = isSingular @@ -48,12 +61,12 @@ export default function NewExternalWorktreesInboxLine({ ? translate( 'auto.components.sidebar.NewExternalWorktreesInboxLine.7f18c5b0d3', 'Review {{value0}} hidden worktree in {{value1}}', - { value0: inboxCount, value1: repoDisplayName } + { value0: inboxCount, value1: repoScopeLabel } ) : translate( 'auto.components.sidebar.NewExternalWorktreesInboxLine.4e2b7a9c05', 'Review {{value0}} hidden worktrees in {{value1}}', - { value0: inboxCount, value1: repoDisplayName } + { value0: inboxCount, value1: repoScopeLabel } ) if (inboxCount === 0) { @@ -83,6 +96,11 @@ export default function NewExternalWorktreesInboxLine({ {inboxCount} {countLabel} + {hostContextLabel ? ( + + {hostContextLabel} + + ) : null}