diff --git a/src/main/runtime/rpc/methods/client-ui-schemas.ts b/src/main/runtime/rpc/methods/client-ui-schemas.ts
index f7236c61df3..7265a1ee806 100644
--- a/src/main/runtime/rpc/methods/client-ui-schemas.ts
+++ b/src/main/runtime/rpc/methods/client-ui-schemas.ts
@@ -198,6 +198,7 @@ export const UiUpdate = z
hideDefaultBranchWorkspace: z.boolean().optional(),
hideAutomationGeneratedWorkspaces: z.boolean().optional(),
hideCliCreatedWorkspaces: z.boolean().optional(),
+ hideDetachedHeadWorkspaces: z.boolean().optional(),
filterRepoIds: StringArray.optional(),
collapsedGroups: StringArray.optional(),
uiZoomLevel: z.number().finite().optional(),
diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx
index 842a7a9e213..a58f1c5c474 100644
--- a/src/renderer/src/App.tsx
+++ b/src/renderer/src/App.tsx
@@ -639,6 +639,7 @@ function App(): React.JSX.Element {
const hideDefaultBranchWorkspace = useAppStore((s) => s.hideDefaultBranchWorkspace)
const hideAutomationGeneratedWorkspaces = useAppStore((s) => s.hideAutomationGeneratedWorkspaces)
const hideCliCreatedWorkspaces = useAppStore((s) => s.hideCliCreatedWorkspaces)
+ const hideDetachedHeadWorkspaces = useAppStore((s) => s.hideDetachedHeadWorkspaces)
const showDotfilesByWorktree = useAppStore((s) => s.showDotfilesByWorktree)
const filterRepoIds = useAppStore((s) => s.filterRepoIds)
const acknowledgedAgentsByPaneKey = useAppStore((s) => s.acknowledgedAgentsByPaneKey)
@@ -1339,6 +1340,7 @@ function App(): React.JSX.Element {
hideDefaultBranchWorkspace,
hideAutomationGeneratedWorkspaces,
hideCliCreatedWorkspaces,
+ hideDetachedHeadWorkspaces,
showDotfilesByWorktree,
filterRepoIds,
// Why (#9002): activeView is deliberately NOT included here. It used to
@@ -1370,6 +1372,7 @@ function App(): React.JSX.Element {
hideDefaultBranchWorkspace,
hideAutomationGeneratedWorkspaces,
hideCliCreatedWorkspaces,
+ hideDetachedHeadWorkspaces,
showDotfilesByWorktree,
filterRepoIds,
acknowledgedAgentsByPaneKey
diff --git a/src/renderer/src/components/WorktreeJumpPalette.tsx b/src/renderer/src/components/WorktreeJumpPalette.tsx
index 538615f1824..242ce097d7b 100644
--- a/src/renderer/src/components/WorktreeJumpPalette.tsx
+++ b/src/renderer/src/components/WorktreeJumpPalette.tsx
@@ -31,6 +31,7 @@ import { sortWorktreesSmart } from '@/components/sidebar/smart-sort'
import {
isAutomationGeneratedWorkspace,
isCliCreatedWorkspace,
+ isDetachedHeadWorkspace,
isDefaultBranchWorkspace
} from '@/components/sidebar/visible-worktrees'
import { getLiveAgentStatusByWorktreeId, isInactiveWorkspace } from '@/lib/worktree-activity-state'
@@ -394,6 +395,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
const hideDefaultBranchWorkspace = useAppStore((s) => s.hideDefaultBranchWorkspace)
const hideAutomationGeneratedWorkspaces = useAppStore((s) => s.hideAutomationGeneratedWorkspaces)
const hideCliCreatedWorkspaces = useAppStore((s) => s.hideCliCreatedWorkspaces)
+ const hideDetachedHeadWorkspaces = useAppStore((s) => s.hideDetachedHeadWorkspaces)
const showSleepingWorkspaces = useAppStore((s) => s.showSleepingWorkspaces)
const lastVisitedAtByWorktreeId = useAppStore((s) => s.lastVisitedAtByWorktreeId)
const workspacePortScan = useAppStore((s) => s.workspacePortScan?.result ?? null)
@@ -487,6 +489,9 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
if (hideCliCreatedWorkspaces && isCliCreatedWorkspace(worktree)) {
return false
}
+ if (hideDetachedHeadWorkspaces && isDetachedHeadWorkspace(worktree)) {
+ return false
+ }
if (
!showSleepingWorkspaces &&
isInactiveWorkspace(
@@ -507,6 +512,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
hideAutomationGeneratedWorkspaces,
hideCliCreatedWorkspaces,
hideDefaultBranchWorkspace,
+ hideDetachedHeadWorkspaces,
ptyIdsByTabId,
showSleepingWorkspaces,
tabsByWorktree,
diff --git a/src/renderer/src/components/sidebar/SidebarFilter.tsx b/src/renderer/src/components/sidebar/SidebarFilter.tsx
index a594144cb34..06c445564a7 100644
--- a/src/renderer/src/components/sidebar/SidebarFilter.tsx
+++ b/src/renderer/src/components/sidebar/SidebarFilter.tsx
@@ -4,6 +4,7 @@ import {
Check,
FolderPlus,
GitBranch,
+ GitCommitHorizontal,
ListFilter,
Moon,
Server,
@@ -59,6 +60,8 @@ const SidebarFilter = React.memo(function SidebarFilter({
)
const hideCliCreatedWorkspaces = useAppStore((s) => s.hideCliCreatedWorkspaces)
const setHideCliCreatedWorkspaces = useAppStore((s) => s.setHideCliCreatedWorkspaces)
+ const hideDetachedHeadWorkspaces = useAppStore((s) => s.hideDetachedHeadWorkspaces)
+ const setHideDetachedHeadWorkspaces = useAppStore((s) => s.setHideDetachedHeadWorkspaces)
const filterRepoIds = useAppStore((s) => s.filterRepoIds)
const setFilterRepoIds = useAppStore((s) => s.setFilterRepoIds)
const repos = useAppStore((s) => s.repos)
@@ -110,12 +113,14 @@ const SidebarFilter = React.memo(function SidebarFilter({
hideDefaultBranchWorkspace ||
hideAutomationGeneratedWorkspaces ||
hideCliCreatedWorkspaces ||
+ hideDetachedHeadWorkspaces ||
hasRepoFilter
const activeFilterCount =
(hasSleepingFilter ? 1 : 0) +
(hideDefaultBranchWorkspace ? 1 : 0) +
(hideAutomationGeneratedWorkspaces ? 1 : 0) +
(hideCliCreatedWorkspaces ? 1 : 0) +
+ (hideDetachedHeadWorkspaces ? 1 : 0) +
selectedCount
const filteredRepos = useMemo(() => searchRepos(repos, query), [repos, query])
@@ -130,12 +135,14 @@ const SidebarFilter = React.memo(function SidebarFilter({
setHideDefaultBranchWorkspace(false)
setHideAutomationGeneratedWorkspaces(false)
setHideCliCreatedWorkspaces(false)
+ setHideDetachedHeadWorkspaces(false)
setFilterRepoIds([])
}, [
setShowSleepingWorkspaces,
setHideDefaultBranchWorkspace,
setHideAutomationGeneratedWorkspaces,
setHideCliCreatedWorkspaces,
+ setHideDetachedHeadWorkspaces,
setFilterRepoIds
])
@@ -229,6 +236,15 @@ const SidebarFilter = React.memo(function SidebarFilter({
checked={hideCliCreatedWorkspaces}
onChange={setHideCliCreatedWorkspaces}
/>
+ }
+ label={translate(
+ 'auto.components.sidebar.SidebarFilter.detachedHead',
+ 'Hide detached HEAD'
+ )}
+ checked={hideDetachedHeadWorkspaces}
+ onChange={setHideDetachedHeadWorkspaces}
+ />
{canFilterRepos && (
<>
diff --git a/src/renderer/src/components/sidebar/SidebarWorkspaceFilterSection.tsx b/src/renderer/src/components/sidebar/SidebarWorkspaceFilterSection.tsx
index 3469d2d24df..20cddcfae49 100644
--- a/src/renderer/src/components/sidebar/SidebarWorkspaceFilterSection.tsx
+++ b/src/renderer/src/components/sidebar/SidebarWorkspaceFilterSection.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import { CalendarClock, GitBranch, Moon, SquareTerminal } from 'lucide-react'
+import { CalendarClock, GitBranch, GitCommitHorizontal, Moon, SquareTerminal } from 'lucide-react'
import { useAppStore } from '@/store'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
@@ -15,6 +15,8 @@ const SidebarWorkspaceFilterSection = React.memo(function SidebarWorkspaceFilter
)
const hideCliCreatedWorkspaces = useAppStore((s) => s.hideCliCreatedWorkspaces)
const setHideCliCreatedWorkspaces = useAppStore((s) => s.setHideCliCreatedWorkspaces)
+ const hideDetachedHeadWorkspaces = useAppStore((s) => s.hideDetachedHeadWorkspaces)
+ const setHideDetachedHeadWorkspaces = useAppStore((s) => s.setHideDetachedHeadWorkspaces)
return (
<>
@@ -59,6 +61,15 @@ const SidebarWorkspaceFilterSection = React.memo(function SidebarWorkspaceFilter
checked={hideCliCreatedWorkspaces}
onChange={setHideCliCreatedWorkspaces}
/>
+ }
+ label={translate(
+ 'auto.components.sidebar.SidebarWorkspaceFilterSection.detachedHead',
+ 'Hide detached HEAD'
+ )}
+ checked={hideDetachedHeadWorkspaces}
+ onChange={setHideDetachedHeadWorkspaces}
+ />
>
)
})
diff --git a/src/renderer/src/components/sidebar/SidebarWorkspaceOptionsMenu.tsx b/src/renderer/src/components/sidebar/SidebarWorkspaceOptionsMenu.tsx
index 139ad4ae933..b087a3fb8f5 100644
--- a/src/renderer/src/components/sidebar/SidebarWorkspaceOptionsMenu.tsx
+++ b/src/renderer/src/components/sidebar/SidebarWorkspaceOptionsMenu.tsx
@@ -39,6 +39,7 @@ const SidebarWorkspaceOptionsMenu = React.memo(function SidebarWorkspaceOptionsM
const hideDefaultBranchWorkspace = useAppStore((s) => s.hideDefaultBranchWorkspace)
const hideAutomationGeneratedWorkspaces = useAppStore((s) => s.hideAutomationGeneratedWorkspaces)
const hideCliCreatedWorkspaces = useAppStore((s) => s.hideCliCreatedWorkspaces)
+ const hideDetachedHeadWorkspaces = useAppStore((s) => s.hideDetachedHeadWorkspaces)
const filterRepoIds = useAppStore((s) => s.filterRepoIds)
const repos = useAppStore((s) => s.repos)
const setWorkspaceHostScope = useAppStore((s) => s.setWorkspaceHostScope)
@@ -82,6 +83,7 @@ const SidebarWorkspaceOptionsMenu = React.memo(function SidebarWorkspaceOptionsM
hideDefaultBranchWorkspace ||
hideAutomationGeneratedWorkspaces ||
hideCliCreatedWorkspaces ||
+ hideDetachedHeadWorkspaces ||
hasRepoFilter ||
hasHostVisibilityFilter
const activeFilterCount =
@@ -89,6 +91,7 @@ const SidebarWorkspaceOptionsMenu = React.memo(function SidebarWorkspaceOptionsM
(hideDefaultBranchWorkspace ? 1 : 0) +
(hideAutomationGeneratedWorkspaces ? 1 : 0) +
(hideCliCreatedWorkspaces ? 1 : 0) +
+ (hideDetachedHeadWorkspaces ? 1 : 0) +
(hasHostVisibilityFilter ? 1 : 0) +
selectedCount
const activeFilterLabel = `${activeFilterCount} ${activeFilterCount === 1 ? 'filter' : 'filters'}`
diff --git a/src/renderer/src/components/sidebar/WorktreeList.tsx b/src/renderer/src/components/sidebar/WorktreeList.tsx
index 7b96520e706..9cbb151f09c 100644
--- a/src/renderer/src/components/sidebar/WorktreeList.tsx
+++ b/src/renderer/src/components/sidebar/WorktreeList.tsx
@@ -5232,6 +5232,7 @@ const WorktreeList = React.memo(function WorktreeList({
const hideDefaultBranchWorkspace = useAppStore((s) => s.hideDefaultBranchWorkspace)
const hideAutomationGeneratedWorkspaces = useAppStore((s) => s.hideAutomationGeneratedWorkspaces)
const hideCliCreatedWorkspaces = useAppStore((s) => s.hideCliCreatedWorkspaces)
+ const hideDetachedHeadWorkspaces = useAppStore((s) => s.hideDetachedHeadWorkspaces)
const filterRepoIds = useAppStore((s) => s.filterRepoIds)
const openModal = useAppStore((s) => s.openModal)
const openSettingsPage = useAppStore((s) => s.openSettingsPage)
@@ -5518,6 +5519,7 @@ const WorktreeList = React.memo(function WorktreeList({
hideDefaultBranchWorkspace,
hideAutomationGeneratedWorkspaces,
hideCliCreatedWorkspaces,
+ hideDetachedHeadWorkspaces,
repoMap,
workspaceHostScope,
visibleWorkspaceHostIds,
@@ -5534,6 +5536,7 @@ const WorktreeList = React.memo(function WorktreeList({
hideDefaultBranchWorkspace,
hideAutomationGeneratedWorkspaces,
hideCliCreatedWorkspaces,
+ hideDetachedHeadWorkspaces,
workspaceHostScope,
visibleWorkspaceHostIds,
settings,
@@ -6494,6 +6497,7 @@ const WorktreeList = React.memo(function WorktreeList({
hideDefaultBranchWorkspace,
hideAutomationGeneratedWorkspaces,
hideCliCreatedWorkspaces,
+ hideDetachedHeadWorkspaces,
visibleWorkspaceHostIds,
workspaceHostScope
}),
@@ -6503,6 +6507,7 @@ const WorktreeList = React.memo(function WorktreeList({
hideDefaultBranchWorkspace,
hideAutomationGeneratedWorkspaces,
hideCliCreatedWorkspaces,
+ hideDetachedHeadWorkspaces,
visibleWorkspaceHostIds,
workspaceHostScope
]
@@ -6514,6 +6519,7 @@ const WorktreeList = React.memo(function WorktreeList({
(s) => s.setHideAutomationGeneratedWorkspaces
)
const setHideCliCreatedWorkspaces = useAppStore((s) => s.setHideCliCreatedWorkspaces)
+ const setHideDetachedHeadWorkspaces = useAppStore((s) => s.setHideDetachedHeadWorkspaces)
const setFilterRepoIds = useAppStore((s) => s.setFilterRepoIds)
const setVisibleWorkspaceHostIds = useAppStore((s) => s.setVisibleWorkspaceHostIds)
@@ -6534,6 +6540,9 @@ const WorktreeList = React.memo(function WorktreeList({
if (actions.resetHideCliCreatedWorkspaces) {
setHideCliCreatedWorkspaces(false)
}
+ if (actions.resetHideDetachedHeadWorkspaces) {
+ setHideDetachedHeadWorkspaces(false)
+ }
if (actions.resetVisibleWorkspaceHostIds) {
setVisibleWorkspaceHostIds(null)
}
@@ -6543,6 +6552,7 @@ const WorktreeList = React.memo(function WorktreeList({
setHideDefaultBranchWorkspace,
setHideAutomationGeneratedWorkspaces,
setHideCliCreatedWorkspaces,
+ setHideDetachedHeadWorkspaces,
setVisibleWorkspaceHostIds,
filterState
])
diff --git a/src/renderer/src/components/sidebar/sidebar-filter-state.test.ts b/src/renderer/src/components/sidebar/sidebar-filter-state.test.ts
new file mode 100644
index 00000000000..0d27355c45a
--- /dev/null
+++ b/src/renderer/src/components/sidebar/sidebar-filter-state.test.ts
@@ -0,0 +1,217 @@
+import { describe, expect, it } from 'vitest'
+import {
+ computeClearFilterActions,
+ isDefaultBranchWorkspace,
+ sidebarHasActiveFilters
+} from './visible-worktrees'
+import type { Worktree } from '../../../../shared/types'
+
+function makeWorktree(id: string, repoId = 'repo1'): Worktree {
+ return {
+ id,
+ repoId,
+ path: `/tmp/${id}`,
+ head: 'abc123',
+ branch: 'refs/heads/main',
+ isBare: false,
+ isMainWorktree: false,
+ displayName: id,
+ comment: '',
+ linkedIssue: null,
+ linkedPR: null,
+ linkedLinearIssue: null,
+ isArchived: false,
+ isUnread: false,
+ isPinned: false,
+ sortOrder: 0,
+ lastActivityAt: 0
+ }
+}
+
+type FilterState = Parameters[0]
+
+function filterState(overrides: Partial = {}): FilterState {
+ return {
+ showSleepingWorkspaces: true,
+ filterRepoIds: [],
+ hideDefaultBranchWorkspace: false,
+ hideAutomationGeneratedWorkspaces: false,
+ hideCliCreatedWorkspaces: false,
+ hideDetachedHeadWorkspaces: false,
+ workspaceHostScope: 'all',
+ ...overrides
+ }
+}
+
+describe('isDefaultBranchWorkspace', () => {
+ it('returns true for a branch-backed main worktree', () => {
+ const main = makeWorktree('main')
+ main.isMainWorktree = true
+ expect(isDefaultBranchWorkspace(main)).toBe(true)
+ })
+
+ it('returns false for folder-mode main worktrees (empty branch)', () => {
+ const folder = makeWorktree('folder')
+ folder.isMainWorktree = true
+ folder.branch = ''
+ expect(isDefaultBranchWorkspace(folder)).toBe(false)
+ })
+
+ it('returns false for non-main worktrees even on the default branch', () => {
+ const feature = makeWorktree('feature')
+ expect(isDefaultBranchWorkspace(feature)).toBe(false)
+ })
+})
+
+describe('sidebarHasActiveFilters', () => {
+ it('returns false when no filters are active', () => {
+ expect(sidebarHasActiveFilters(filterState())).toBe(false)
+ })
+
+ it('returns true when only hideDefaultBranchWorkspace is active', () => {
+ // Why: regression guard for the empty-sidebar escape hatch. If hide is
+ // omitted from the filter union, a user whose only worktree is the
+ // default-branch row sees "No workspaces found" with no way back.
+ expect(sidebarHasActiveFilters(filterState({ hideDefaultBranchWorkspace: true }))).toBe(true)
+ })
+
+ it('returns true when only automation-created workspaces are hidden', () => {
+ expect(sidebarHasActiveFilters(filterState({ hideAutomationGeneratedWorkspaces: true }))).toBe(
+ true
+ )
+ })
+
+ it('returns true when only CLI-created workspaces are hidden', () => {
+ expect(sidebarHasActiveFilters(filterState({ hideCliCreatedWorkspaces: true }))).toBe(true)
+ })
+
+ it('returns true when only detached-HEAD workspaces are hidden', () => {
+ expect(sidebarHasActiveFilters(filterState({ hideDetachedHeadWorkspaces: true }))).toBe(true)
+ })
+
+ it('returns true when sleeping workspaces are hidden', () => {
+ expect(sidebarHasActiveFilters(filterState({ showSleepingWorkspaces: false }))).toBe(true)
+ })
+
+ it('returns true when only filterRepoIds is non-empty', () => {
+ expect(sidebarHasActiveFilters(filterState({ filterRepoIds: ['repo1'] }))).toBe(true)
+ })
+
+ it('returns true when only host visibility is narrowed', () => {
+ expect(sidebarHasActiveFilters(filterState({ visibleWorkspaceHostIds: ['local'] }))).toBe(true)
+ })
+})
+
+describe('computeClearFilterActions', () => {
+ it('returns no-op actions when nothing is set', () => {
+ expect(computeClearFilterActions(filterState())).toEqual({
+ resetShowSleepingWorkspaces: false,
+ resetFilterRepoIds: false,
+ resetHideDefaultBranchWorkspace: false,
+ resetHideAutomationGeneratedWorkspaces: false,
+ resetHideCliCreatedWorkspaces: false,
+ resetHideDetachedHeadWorkspaces: false,
+ resetVisibleWorkspaceHostIds: false
+ })
+ })
+
+ it('flags only hideDefaultBranchWorkspace for reset when it is the sole filter', () => {
+ // Why: verifies the empty-sidebar escape hatch actually clears the hide
+ // flag. A regression here would leave users stuck on "No workspaces found"
+ // because the only active filter would never clear.
+ expect(computeClearFilterActions(filterState({ hideDefaultBranchWorkspace: true }))).toEqual({
+ resetShowSleepingWorkspaces: false,
+ resetFilterRepoIds: false,
+ resetHideDefaultBranchWorkspace: true,
+ resetHideAutomationGeneratedWorkspaces: false,
+ resetHideCliCreatedWorkspaces: false,
+ resetHideDetachedHeadWorkspaces: false,
+ resetVisibleWorkspaceHostIds: false
+ })
+ })
+
+ it('flags only hideAutomationGeneratedWorkspaces for reset when it is the sole filter', () => {
+ expect(
+ computeClearFilterActions(filterState({ hideAutomationGeneratedWorkspaces: true }))
+ ).toEqual({
+ resetShowSleepingWorkspaces: false,
+ resetFilterRepoIds: false,
+ resetHideDefaultBranchWorkspace: false,
+ resetHideAutomationGeneratedWorkspaces: true,
+ resetHideCliCreatedWorkspaces: false,
+ resetHideDetachedHeadWorkspaces: false,
+ resetVisibleWorkspaceHostIds: false
+ })
+ })
+
+ it('flags only hideCliCreatedWorkspaces for reset when it is the sole filter', () => {
+ expect(computeClearFilterActions(filterState({ hideCliCreatedWorkspaces: true }))).toEqual({
+ resetShowSleepingWorkspaces: false,
+ resetFilterRepoIds: false,
+ resetHideDefaultBranchWorkspace: false,
+ resetHideAutomationGeneratedWorkspaces: false,
+ resetHideCliCreatedWorkspaces: true,
+ resetHideDetachedHeadWorkspaces: false,
+ resetVisibleWorkspaceHostIds: false
+ })
+ })
+
+ it('flags only hideDetachedHeadWorkspaces for reset when it is the sole filter', () => {
+ expect(computeClearFilterActions(filterState({ hideDetachedHeadWorkspaces: true }))).toEqual({
+ resetShowSleepingWorkspaces: false,
+ resetFilterRepoIds: false,
+ resetHideDefaultBranchWorkspace: false,
+ resetHideAutomationGeneratedWorkspaces: false,
+ resetHideCliCreatedWorkspaces: false,
+ resetHideDetachedHeadWorkspaces: true,
+ resetVisibleWorkspaceHostIds: false
+ })
+ })
+
+ it('does not flag hideDefaultBranchWorkspace when it is already off', () => {
+ // Why: avoids issuing a pointless IPC write on every Clear Filters click
+ // in the common case where hide was never on.
+ const actions = computeClearFilterActions(
+ filterState({
+ filterRepoIds: ['repo1']
+ })
+ )
+ expect(actions.resetHideDefaultBranchWorkspace).toBe(false)
+ expect(actions.resetShowSleepingWorkspaces).toBe(false)
+ expect(actions.resetFilterRepoIds).toBe(true)
+ })
+
+ it('flags legacy single-host scope for reset even without visible host ids', () => {
+ expect(computeClearFilterActions(filterState({ workspaceHostScope: 'ssh:host-1' }))).toEqual({
+ resetShowSleepingWorkspaces: false,
+ resetFilterRepoIds: false,
+ resetHideDefaultBranchWorkspace: false,
+ resetHideAutomationGeneratedWorkspaces: false,
+ resetHideCliCreatedWorkspaces: false,
+ resetHideDetachedHeadWorkspaces: false,
+ resetVisibleWorkspaceHostIds: true
+ })
+ })
+
+ it('flags every active filter simultaneously', () => {
+ expect(
+ computeClearFilterActions(
+ filterState({
+ showSleepingWorkspaces: false,
+ filterRepoIds: ['repo1', 'repo2'],
+ hideDefaultBranchWorkspace: true,
+ hideAutomationGeneratedWorkspaces: true,
+ visibleWorkspaceHostIds: ['local']
+ })
+ )
+ ).toEqual({
+ resetShowSleepingWorkspaces: true,
+ resetFilterRepoIds: true,
+ resetHideDefaultBranchWorkspace: true,
+ resetHideAutomationGeneratedWorkspaces: true,
+ resetHideCliCreatedWorkspaces: false,
+ resetHideDetachedHeadWorkspaces: false,
+ resetVisibleWorkspaceHostIds: true
+ })
+ })
+})
diff --git a/src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.ts b/src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.ts
index 7edcf8a45f9..15e3ec6de43 100644
--- a/src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.ts
+++ b/src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.ts
@@ -21,6 +21,7 @@ export function useVisibleWorkspaceKanbanWorktreeIds({
const hideDefaultBranchWorkspace = useAppStore((s) => s.hideDefaultBranchWorkspace)
const hideAutomationGeneratedWorkspaces = useAppStore((s) => s.hideAutomationGeneratedWorkspaces)
const hideCliCreatedWorkspaces = useAppStore((s) => s.hideCliCreatedWorkspaces)
+ const hideDetachedHeadWorkspaces = useAppStore((s) => s.hideDetachedHeadWorkspaces)
const workspaceHostScope = useAppStore((s) => s.workspaceHostScope)
const visibleWorkspaceHostIds = useAppStore((s) => s.visibleWorkspaceHostIds)
const settings = useAppStore((s) => s.settings)
@@ -59,6 +60,7 @@ export function useVisibleWorkspaceKanbanWorktreeIds({
hideDefaultBranchWorkspace,
hideAutomationGeneratedWorkspaces,
hideCliCreatedWorkspaces,
+ hideDetachedHeadWorkspaces,
repoMap,
workspaceHostScope,
visibleWorkspaceHostIds,
@@ -76,6 +78,7 @@ export function useVisibleWorkspaceKanbanWorktreeIds({
hideDefaultBranchWorkspace,
hideAutomationGeneratedWorkspaces,
hideCliCreatedWorkspaces,
+ hideDetachedHeadWorkspaces,
workspaceHostScope,
visibleWorkspaceHostIds,
settings,
diff --git a/src/renderer/src/components/sidebar/visible-worktrees.test.ts b/src/renderer/src/components/sidebar/visible-worktrees.test.ts
index 7c3ff699334..f54dcef60e7 100644
--- a/src/renderer/src/components/sidebar/visible-worktrees.test.ts
+++ b/src/renderer/src/components/sidebar/visible-worktrees.test.ts
@@ -1,10 +1,5 @@
import { describe, expect, it } from 'vitest'
-import {
- computeClearFilterActions,
- computeVisibleWorktreeIds,
- isDefaultBranchWorkspace,
- sidebarHasActiveFilters
-} from './visible-worktrees'
+import { computeVisibleWorktreeIds } from './visible-worktrees'
import type { Repo, TerminalTab, Worktree, WorktreeLineage } from '../../../../shared/types'
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
@@ -83,6 +78,7 @@ function visibleOptions(overrides: Partial = {}): VisibleOptions
hideDefaultBranchWorkspace: false,
hideAutomationGeneratedWorkspaces: false,
hideCliCreatedWorkspaces: false,
+ hideDetachedHeadWorkspaces: false,
repoMap,
workspaceHostScope: 'all',
defaultHostId: LOCAL_EXECUTION_HOST_ID,
@@ -91,20 +87,6 @@ function visibleOptions(overrides: Partial = {}): VisibleOptions
}
}
-type FilterState = Parameters[0]
-
-function filterState(overrides: Partial = {}): FilterState {
- return {
- showSleepingWorkspaces: true,
- filterRepoIds: [],
- hideDefaultBranchWorkspace: false,
- hideAutomationGeneratedWorkspaces: false,
- hideCliCreatedWorkspaces: false,
- workspaceHostScope: 'all',
- ...overrides
- }
-}
-
describe('computeVisibleWorktreeIds', () => {
it('keeps browser-tab worktrees visible when sleeping workspaces are hidden', () => {
const wt = makeWorktree('wt-browser')
@@ -216,6 +198,46 @@ describe('computeVisibleWorktreeIds', () => {
expect(result).toEqual([legacy.id])
})
+ it('hides detached-HEAD workspaces when the detached filter is enabled', () => {
+ const onBranch = makeWorktree('on-branch')
+ const detached = { ...makeWorktree('detached'), branch: '', head: 'deadbeefcafe' }
+
+ const result = computeVisibleWorktreeIds(
+ { repo1: [onBranch, detached] },
+ [onBranch.id, detached.id],
+ visibleOptions({ hideDetachedHeadWorkspaces: true })
+ )
+
+ expect(result).toEqual([onBranch.id])
+ })
+
+ it('keeps detached-HEAD workspaces visible while the detached filter is off', () => {
+ const onBranch = makeWorktree('on-branch')
+ const detached = { ...makeWorktree('detached'), branch: '', head: 'deadbeefcafe' }
+
+ const result = computeVisibleWorktreeIds(
+ { repo1: [onBranch, detached] },
+ [onBranch.id, detached.id],
+ visibleOptions()
+ )
+
+ expect(result).toEqual([onBranch.id, detached.id])
+ })
+
+ it('keeps headless workspaces visible when the detached filter is enabled', () => {
+ // Why: folder workspaces and SSH-synthesized rows carry an empty branch AND
+ // an empty head. Only a real head means a genuine detached checkout.
+ const folder = { ...makeWorktree('folder'), branch: '', head: '', isMainWorktree: true }
+
+ const result = computeVisibleWorktreeIds(
+ { repo1: [folder] },
+ [folder.id],
+ visibleOptions({ hideDetachedHeadWorkspaces: true })
+ )
+
+ expect(result).toEqual([folder.id])
+ })
+
it('does not treat slept wake-hint tabs as live surfaces', () => {
const wt = makeWorktree('wt-slept')
@@ -596,6 +618,11 @@ describe('computeVisibleWorktreeIds', () => {
const cliParent = makeWorktree('cli-parent')
cliParent.cliProvenance = { kind: 'created-by-cli', createdAt: 1 }
expect(run(cliParent, { hideCliCreatedWorkspaces: true })).toEqual([child.id])
+
+ const detachedParent = makeWorktree('detached-parent')
+ detachedParent.branch = ''
+ detachedParent.head = 'deadbeefcafe'
+ expect(run(detachedParent, { hideDetachedHeadWorkspaces: true })).toEqual([child.id])
})
it('includes inline lineage ancestors when send-target mode forces a filtered child visible', () => {
@@ -751,154 +778,3 @@ describe('computeVisibleWorktreeIds', () => {
expect(result).toEqual([child.id])
})
})
-
-describe('isDefaultBranchWorkspace', () => {
- it('returns true for a branch-backed main worktree', () => {
- const main = makeWorktree('main')
- main.isMainWorktree = true
- expect(isDefaultBranchWorkspace(main)).toBe(true)
- })
-
- it('returns false for folder-mode main worktrees (empty branch)', () => {
- const folder = makeWorktree('folder')
- folder.isMainWorktree = true
- folder.branch = ''
- expect(isDefaultBranchWorkspace(folder)).toBe(false)
- })
-
- it('returns false for non-main worktrees even on the default branch', () => {
- const feature = makeWorktree('feature')
- expect(isDefaultBranchWorkspace(feature)).toBe(false)
- })
-})
-
-describe('sidebarHasActiveFilters', () => {
- it('returns false when no filters are active', () => {
- expect(sidebarHasActiveFilters(filterState())).toBe(false)
- })
-
- it('returns true when only hideDefaultBranchWorkspace is active', () => {
- // Why: regression guard for the empty-sidebar escape hatch. If hide is
- // omitted from the filter union, a user whose only worktree is the
- // default-branch row sees "No workspaces found" with no way back.
- expect(sidebarHasActiveFilters(filterState({ hideDefaultBranchWorkspace: true }))).toBe(true)
- })
-
- it('returns true when only automation-created workspaces are hidden', () => {
- expect(sidebarHasActiveFilters(filterState({ hideAutomationGeneratedWorkspaces: true }))).toBe(
- true
- )
- })
-
- it('returns true when only CLI-created workspaces are hidden', () => {
- expect(sidebarHasActiveFilters(filterState({ hideCliCreatedWorkspaces: true }))).toBe(true)
- })
-
- it('returns true when sleeping workspaces are hidden', () => {
- expect(sidebarHasActiveFilters(filterState({ showSleepingWorkspaces: false }))).toBe(true)
- })
-
- it('returns true when only filterRepoIds is non-empty', () => {
- expect(sidebarHasActiveFilters(filterState({ filterRepoIds: ['repo1'] }))).toBe(true)
- })
-
- it('returns true when only host visibility is narrowed', () => {
- expect(sidebarHasActiveFilters(filterState({ visibleWorkspaceHostIds: ['local'] }))).toBe(true)
- })
-})
-
-describe('computeClearFilterActions', () => {
- it('returns no-op actions when nothing is set', () => {
- expect(computeClearFilterActions(filterState())).toEqual({
- resetShowSleepingWorkspaces: false,
- resetFilterRepoIds: false,
- resetHideDefaultBranchWorkspace: false,
- resetHideAutomationGeneratedWorkspaces: false,
- resetHideCliCreatedWorkspaces: false,
- resetVisibleWorkspaceHostIds: false
- })
- })
-
- it('flags only hideDefaultBranchWorkspace for reset when it is the sole filter', () => {
- // Why: verifies the empty-sidebar escape hatch actually clears the hide
- // flag. A regression here would leave users stuck on "No workspaces found"
- // because the only active filter would never clear.
- expect(computeClearFilterActions(filterState({ hideDefaultBranchWorkspace: true }))).toEqual({
- resetShowSleepingWorkspaces: false,
- resetFilterRepoIds: false,
- resetHideDefaultBranchWorkspace: true,
- resetHideAutomationGeneratedWorkspaces: false,
- resetHideCliCreatedWorkspaces: false,
- resetVisibleWorkspaceHostIds: false
- })
- })
-
- it('flags only hideAutomationGeneratedWorkspaces for reset when it is the sole filter', () => {
- expect(
- computeClearFilterActions(filterState({ hideAutomationGeneratedWorkspaces: true }))
- ).toEqual({
- resetShowSleepingWorkspaces: false,
- resetFilterRepoIds: false,
- resetHideDefaultBranchWorkspace: false,
- resetHideAutomationGeneratedWorkspaces: true,
- resetHideCliCreatedWorkspaces: false,
- resetVisibleWorkspaceHostIds: false
- })
- })
-
- it('flags only hideCliCreatedWorkspaces for reset when it is the sole filter', () => {
- expect(computeClearFilterActions(filterState({ hideCliCreatedWorkspaces: true }))).toEqual({
- resetShowSleepingWorkspaces: false,
- resetFilterRepoIds: false,
- resetHideDefaultBranchWorkspace: false,
- resetHideAutomationGeneratedWorkspaces: false,
- resetHideCliCreatedWorkspaces: true,
- resetVisibleWorkspaceHostIds: false
- })
- })
-
- it('does not flag hideDefaultBranchWorkspace when it is already off', () => {
- // Why: avoids issuing a pointless IPC write on every Clear Filters click
- // in the common case where hide was never on.
- const actions = computeClearFilterActions(
- filterState({
- filterRepoIds: ['repo1']
- })
- )
- expect(actions.resetHideDefaultBranchWorkspace).toBe(false)
- expect(actions.resetShowSleepingWorkspaces).toBe(false)
- expect(actions.resetFilterRepoIds).toBe(true)
- })
-
- it('flags legacy single-host scope for reset even without visible host ids', () => {
- expect(computeClearFilterActions(filterState({ workspaceHostScope: 'ssh:host-1' }))).toEqual({
- resetShowSleepingWorkspaces: false,
- resetFilterRepoIds: false,
- resetHideDefaultBranchWorkspace: false,
- resetHideAutomationGeneratedWorkspaces: false,
- resetHideCliCreatedWorkspaces: false,
- resetVisibleWorkspaceHostIds: true
- })
- })
-
- it('flags every active filter simultaneously', () => {
- expect(
- computeClearFilterActions(
- filterState({
- showSleepingWorkspaces: false,
- filterRepoIds: ['repo1', 'repo2'],
- hideDefaultBranchWorkspace: true,
- hideAutomationGeneratedWorkspaces: true,
- visibleWorkspaceHostIds: ['local']
- })
- )
- ).toEqual({
- resetShowSleepingWorkspaces: true,
- resetFilterRepoIds: true,
- resetHideDefaultBranchWorkspace: true,
- resetHideAutomationGeneratedWorkspaces: true,
- resetHideCliCreatedWorkspaces: false,
- resetVisibleWorkspaceHostIds: true
- })
- })
-})
diff --git a/src/renderer/src/components/sidebar/visible-worktrees.ts b/src/renderer/src/components/sidebar/visible-worktrees.ts
index 2ee9f41a0b2..9c44d8587e3 100644
--- a/src/renderer/src/components/sidebar/visible-worktrees.ts
+++ b/src/renderer/src/components/sidebar/visible-worktrees.ts
@@ -20,6 +20,7 @@ import {
getLineageRenderInfo
} from './worktree-lineage-projection'
import { computeRenderedSidebarWorktreeOrder } from './rendered-sidebar-worktree-order'
+import { getWorktreeGitIdentityDisplay } from '@/lib/worktree-git-identity-display'
/**
* Whether a worktree represents the repo's default-branch row that the
@@ -42,6 +43,18 @@ export function isCliCreatedWorkspace(worktree: Worktree): boolean {
return worktree.cliProvenance?.kind === 'created-by-cli'
}
+/**
+ * Whether a worktree sits on a detached HEAD (a commit, not a branch).
+ *
+ * Why the head check: folder workspaces and SSH-synthesized rows carry both an
+ * empty branch and an empty head, so branch-emptiness alone would sweep them
+ * into this filter. Requiring a real head keeps the predicate to genuine
+ * detached-HEAD checkouts, matching what DetachedHeadBadge renders on the card.
+ */
+export function isDetachedHeadWorkspace(worktree: Worktree): boolean {
+ return getWorktreeGitIdentityDisplay(worktree)?.kind === 'detached'
+}
+
/** Inputs describing sidebar filter settings that the Clear Filters path owns. */
export type SidebarFilterState = {
showSleepingWorkspaces: boolean
@@ -49,6 +62,7 @@ export type SidebarFilterState = {
hideDefaultBranchWorkspace: boolean
hideAutomationGeneratedWorkspaces: boolean
hideCliCreatedWorkspaces: boolean
+ hideDetachedHeadWorkspaces: boolean
visibleWorkspaceHostIds?: readonly ExecutionHostId[] | null
workspaceHostScope?: ExecutionHostScope
}
@@ -69,6 +83,7 @@ export function sidebarHasActiveFilters(state: SidebarFilterState): boolean {
state.hideDefaultBranchWorkspace ||
state.hideAutomationGeneratedWorkspaces ||
state.hideCliCreatedWorkspaces ||
+ state.hideDetachedHeadWorkspaces ||
state.visibleWorkspaceHostIds != null ||
(state.workspaceHostScope != null && state.workspaceHostScope !== ALL_EXECUTION_HOSTS_SCOPE)
)
@@ -82,6 +97,7 @@ export type ClearFilterActions = {
resetHideDefaultBranchWorkspace: boolean
resetHideAutomationGeneratedWorkspaces: boolean
resetHideCliCreatedWorkspaces: boolean
+ resetHideDetachedHeadWorkspaces: boolean
resetVisibleWorkspaceHostIds: boolean
}
@@ -102,6 +118,7 @@ export function computeClearFilterActions(state: SidebarFilterState): ClearFilte
resetHideDefaultBranchWorkspace: state.hideDefaultBranchWorkspace,
resetHideAutomationGeneratedWorkspaces: state.hideAutomationGeneratedWorkspaces,
resetHideCliCreatedWorkspaces: state.hideCliCreatedWorkspaces,
+ resetHideDetachedHeadWorkspaces: state.hideDetachedHeadWorkspaces,
resetVisibleWorkspaceHostIds:
state.visibleWorkspaceHostIds != null ||
(state.workspaceHostScope != null && state.workspaceHostScope !== ALL_EXECUTION_HOSTS_SCOPE)
@@ -137,6 +154,7 @@ export function computeVisibleWorktreeIds(
hideDefaultBranchWorkspace: boolean
hideAutomationGeneratedWorkspaces: boolean
hideCliCreatedWorkspaces: boolean
+ hideDetachedHeadWorkspaces: boolean
repoMap: Map
workspaceHostScope: ExecutionHostScope
visibleWorkspaceHostIds?: readonly ExecutionHostId[] | null
@@ -167,6 +185,10 @@ export function computeVisibleWorktreeIds(
all = all.filter((w) => !isCliCreatedWorkspace(w))
}
+ if (opts.hideDetachedHeadWorkspaces) {
+ all = all.filter((w) => !isDetachedHeadWorkspace(w))
+ }
+
const visibleHostIds =
opts.visibleWorkspaceHostIds ??
(opts.workspaceHostScope === ALL_EXECUTION_HOSTS_SCOPE ? null : [opts.workspaceHostScope])
@@ -347,6 +369,7 @@ export function getVisibleWorktreeIds(): string[] {
hideDefaultBranchWorkspace: state.hideDefaultBranchWorkspace,
hideAutomationGeneratedWorkspaces: state.hideAutomationGeneratedWorkspaces,
hideCliCreatedWorkspaces: state.hideCliCreatedWorkspaces,
+ hideDetachedHeadWorkspaces: state.hideDetachedHeadWorkspaces,
repoMap,
workspaceHostScope: state.workspaceHostScope,
visibleWorkspaceHostIds: state.visibleWorkspaceHostIds,
diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json
index 117de2db5ec..d617a59168c 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -4125,7 +4125,8 @@
"489d1c8c9f": "Search projects...",
"ee240a39eb": "Edit filters",
"automationCreated": "Hide automation-created",
- "cliCreated": "Hide CLI-created"
+ "cliCreated": "Hide CLI-created",
+ "detachedHead": "Hide detached HEAD"
},
"SidebarHeader": {
"25a95899c9": "Add Project",
@@ -4189,7 +4190,8 @@
"ed1611b65b": "Hide sleeping",
"82594419ba": "Filters",
"automationCreated": "Hide automation-created",
- "cliCreated": "Hide CLI-created"
+ "cliCreated": "Hide CLI-created",
+ "detachedHead": "Hide detached HEAD"
},
"sidebarHostOptions": {
"3e102f111c": "All hosts",
diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json
index 37222a3a307..a92e2d06a08 100644
--- a/src/renderer/src/i18n/locales/es.json
+++ b/src/renderer/src/i18n/locales/es.json
@@ -4087,7 +4087,8 @@
"489d1c8c9f": "Buscar proyectos...",
"ee240a39eb": "Editar filtros",
"automationCreated": "Ocultar creados por automatizaciones",
- "cliCreated": "Hide CLI-created"
+ "cliCreated": "Hide CLI-created",
+ "detachedHead": "Ocultar HEAD desacoplado"
},
"SidebarHeader": {
"92154beb7e": "Nuevo espacio de trabajo",
@@ -4151,7 +4152,8 @@
"ed1611b65b": "Ocultar en reposo",
"82594419ba": "Filtros",
"automationCreated": "Ocultar creados por automatizaciones",
- "cliCreated": "Hide CLI-created"
+ "cliCreated": "Hide CLI-created",
+ "detachedHead": "Ocultar HEAD desacoplado"
},
"SidebarWorkspaceOptionsMenu": {
"95c9754653": "Diseño de actividad del Agent",
diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json
index 398c5c9611d..e519062d530 100644
--- a/src/renderer/src/i18n/locales/ja.json
+++ b/src/renderer/src/i18n/locales/ja.json
@@ -4068,7 +4068,8 @@
"489d1c8c9f": "プロジェクトを検索...",
"ee240a39eb": "フィルターの編集",
"automationCreated": "自動化で作成されたワークスペースを非表示",
- "cliCreated": "Hide CLI-created"
+ "cliCreated": "Hide CLI-created",
+ "detachedHead": "分離HEADを非表示"
},
"SidebarHeader": {
"92154beb7e": "新規ワークスペース",
@@ -4132,7 +4133,8 @@
"ed1611b65b": "スリープ中を非表示",
"82594419ba": "フィルター",
"automationCreated": "自動化で作成されたワークスペースを非表示",
- "cliCreated": "Hide CLI-created"
+ "cliCreated": "Hide CLI-created",
+ "detachedHead": "分離HEADを非表示"
},
"SidebarWorkspaceOptionsMenu": {
"95c9754653": "Agent アクティビティのレイアウト",
diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json
index a5150b01853..4e0d9aabee9 100644
--- a/src/renderer/src/i18n/locales/ko.json
+++ b/src/renderer/src/i18n/locales/ko.json
@@ -4068,7 +4068,8 @@
"489d1c8c9f": "프로젝트 검색...",
"ee240a39eb": "필터 편집",
"automationCreated": "자동화로 생성된 워크스페이스 숨기기",
- "cliCreated": "Hide CLI-created"
+ "cliCreated": "Hide CLI-created",
+ "detachedHead": "분리된 HEAD 숨기기"
},
"SidebarHeader": {
"92154beb7e": "새로운 워크스페이스",
@@ -4132,7 +4133,8 @@
"ed1611b65b": "슬립 중인 항목 숨기기",
"82594419ba": "필터",
"automationCreated": "자동화로 생성된 워크스페이스 숨기기",
- "cliCreated": "Hide CLI-created"
+ "cliCreated": "Hide CLI-created",
+ "detachedHead": "분리된 HEAD 숨기기"
},
"SidebarWorkspaceOptionsMenu": {
"95c9754653": "Agent 활동 레이아웃",
diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json
index 709f1a053bb..66d232343a0 100644
--- a/src/renderer/src/i18n/locales/zh.json
+++ b/src/renderer/src/i18n/locales/zh.json
@@ -4068,7 +4068,8 @@
"489d1c8c9f": "搜索项目...",
"ee240a39eb": "编辑筛选条件",
"automationCreated": "隐藏自动化创建的工作区",
- "cliCreated": "Hide CLI-created"
+ "cliCreated": "Hide CLI-created",
+ "detachedHead": "隐藏分离 HEAD"
},
"SidebarHeader": {
"92154beb7e": "新工作区",
@@ -4132,7 +4133,8 @@
"ed1611b65b": "隐藏休眠项",
"82594419ba": "筛选条件",
"automationCreated": "隐藏自动化创建的工作区",
- "cliCreated": "Hide CLI-created"
+ "cliCreated": "Hide CLI-created",
+ "detachedHead": "隐藏分离 HEAD"
},
"SidebarWorkspaceOptionsMenu": {
"95c9754653": "智能体活动布局",
diff --git a/src/renderer/src/lib/startup-ui-hydration.ts b/src/renderer/src/lib/startup-ui-hydration.ts
index bee87b3052e..1fb14546ce8 100644
--- a/src/renderer/src/lib/startup-ui-hydration.ts
+++ b/src/renderer/src/lib/startup-ui-hydration.ts
@@ -49,6 +49,7 @@ export function getStartupErrorFallbackUI(uiHydrated: boolean): PersistedUIState
showSleepingWorkspaces: DEFAULT_SHOW_SLEEPING_WORKSPACES,
hideDefaultBranchWorkspace: false,
hideCliCreatedWorkspaces: false,
+ hideDetachedHeadWorkspaces: false,
hideAutomationGeneratedWorkspaces: false,
filterRepoIds: [],
collapsedGroups: [],
diff --git a/src/renderer/src/lib/worktree-activation.ts b/src/renderer/src/lib/worktree-activation.ts
index 5fef7f80e98..19f79bf175a 100644
--- a/src/renderer/src/lib/worktree-activation.ts
+++ b/src/renderer/src/lib/worktree-activation.ts
@@ -64,6 +64,7 @@ import {
import { toast } from 'sonner'
import { initialAgentTabViewModeProps } from './native-chat-initial-view-mode'
import { getConnectionId } from '@/lib/connection-context'
+import { isDetachedHeadWorkspace } from '@/components/sidebar/visible-worktrees'
import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability'
import { seedNativeChatAppliedSessionOptions } from '@/components/native-chat/native-chat-session-option-cache'
import { resolveNativeChatSessionOptionDefaults } from '../../../shared/native-chat-session-option-defaults'
@@ -367,6 +368,9 @@ export function activateAndRevealWorktree(
if (state.hideCliCreatedWorkspaces && wt.cliProvenance?.kind === 'created-by-cli') {
state.setHideCliCreatedWorkspaces(false)
}
+ if (state.hideDetachedHeadWorkspaces && isDetachedHeadWorkspace(wt)) {
+ state.setHideDetachedHeadWorkspaces(false)
+ }
// 6. Reveal in sidebar
if (opts?.revealInSidebar !== false) {
diff --git a/src/renderer/src/store/slices/store-session-cascades.test.ts b/src/renderer/src/store/slices/store-session-cascades.test.ts
index 98761ca5cac..aef50546144 100644
--- a/src/renderer/src/store/slices/store-session-cascades.test.ts
+++ b/src/renderer/src/store/slices/store-session-cascades.test.ts
@@ -1651,6 +1651,7 @@ describe('reconnectPersistedTerminals', () => {
hideDefaultBranchWorkspace: false,
hideAutomationGeneratedWorkspaces: false,
hideCliCreatedWorkspaces: false,
+ hideDetachedHeadWorkspaces: false,
repoMap: new Map(s.repos.map((repo) => [repo.id, repo])),
workspaceHostScope: 'all',
defaultHostId: LOCAL_EXECUTION_HOST_ID,
@@ -1997,6 +1998,7 @@ describe('reconnectPersistedTerminals', () => {
hideDefaultBranchWorkspace: false,
hideAutomationGeneratedWorkspaces: false,
hideCliCreatedWorkspaces: false,
+ hideDetachedHeadWorkspaces: false,
repoMap: new Map(s.repos.map((repo) => [repo.id, repo])),
workspaceHostScope: 'all',
defaultHostId: LOCAL_EXECUTION_HOST_ID,
diff --git a/src/renderer/src/store/slices/ui.ts b/src/renderer/src/store/slices/ui.ts
index 6988e5cc474..a39631edc79 100644
--- a/src/renderer/src/store/slices/ui.ts
+++ b/src/renderer/src/store/slices/ui.ts
@@ -868,6 +868,8 @@ export type UISlice = {
setHideAutomationGeneratedWorkspaces: (v: boolean) => void
hideCliCreatedWorkspaces: boolean
setHideCliCreatedWorkspaces: (v: boolean) => void
+ hideDetachedHeadWorkspaces: boolean
+ setHideDetachedHeadWorkspaces: (v: boolean) => void
showDotfilesByWorktree: Record
setShowDotfilesForWorktree: (worktreeId: string, showDotfiles: boolean) => void
toggleShowDotfilesForWorktree: (worktreeId: string) => void
@@ -2033,6 +2035,8 @@ export const createUISlice: StateCreator = (set, get)
setHideAutomationGeneratedWorkspaces: (v) => set({ hideAutomationGeneratedWorkspaces: v }),
hideCliCreatedWorkspaces: false,
setHideCliCreatedWorkspaces: (v) => set({ hideCliCreatedWorkspaces: v }),
+ hideDetachedHeadWorkspaces: false,
+ setHideDetachedHeadWorkspaces: (v) => set({ hideDetachedHeadWorkspaces: v }),
showDotfilesByWorktree: {},
setShowDotfilesForWorktree: (worktreeId, showDotfiles) =>
@@ -2432,6 +2436,7 @@ export const createUISlice: StateCreator = (set, get)
hideDefaultBranchWorkspace: ui.hideDefaultBranchWorkspace ?? false,
hideAutomationGeneratedWorkspaces: ui.hideAutomationGeneratedWorkspaces === true,
hideCliCreatedWorkspaces: ui.hideCliCreatedWorkspaces === true,
+ hideDetachedHeadWorkspaces: ui.hideDetachedHeadWorkspaces === true,
showDotfilesByWorktree: sanitizeShowDotfilesByWorktree(ui.showDotfilesByWorktree),
// Why: startup hydrates UI before repo catalogs, so defer repo-filter validation to the all-host refresh.
filterRepoIds:
diff --git a/src/shared/constants.ts b/src/shared/constants.ts
index 3b1ff27ed7d..dfafdd6a923 100644
--- a/src/shared/constants.ts
+++ b/src/shared/constants.ts
@@ -460,6 +460,7 @@ export function getDefaultUIState(): PersistedUIState {
hideDefaultBranchWorkspace: false,
hideAutomationGeneratedWorkspaces: false,
hideCliCreatedWorkspaces: false,
+ hideDetachedHeadWorkspaces: false,
showDotfilesByWorktree: {},
filterRepoIds: [],
collapsedGroups: [],
diff --git a/src/shared/types.ts b/src/shared/types.ts
index d94617f7dc1..5838526733d 100644
--- a/src/shared/types.ts
+++ b/src/shared/types.ts
@@ -3265,6 +3265,8 @@ export type PersistedUIState = {
hideAutomationGeneratedWorkspaces?: boolean
/** Hide workspaces created through `orca worktree create`. */
hideCliCreatedWorkspaces?: boolean
+ /** Hide workspaces sitting on a detached HEAD; folder workspaces (no head at all) are unaffected. */
+ hideDetachedHeadWorkspaces?: boolean
/** Per-worktree Explorer dotfile visibility. Missing entries inherit the default: show. */
showDotfilesByWorktree?: Record
filterRepoIds: string[]