mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
feat(sidebar): add a filter to hide detached-HEAD workspaces (#10786)
Adds "Hide detached HEAD" alongside the existing sidebar filters, wired through the same pipeline as Hide CLI-created: sidebar list, Cmd+J empty-query list, workspace board, active-filter badges, Clear/Reset Filters, and persisted UI state. The predicate reuses getWorktreeGitIdentityDisplay so the filter targets exactly what the card renders a Detached HEAD badge for. Requiring a real head (not just an empty branch) keeps folder workspaces and SSH-synthesized rows — which carry both empty — out of the filter. Activating a hidden detached workspace clears the filter, matching the existing reveal escape hatch for automation- and CLI-created workspaces. Splits the filter-state describes out of visible-worktrees.test.ts into sidebar-filter-state.test.ts to stay under the max-lines budget. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
<FilterToggleRow
|
||||
icon={<GitCommitHorizontal className="size-3.5" />}
|
||||
label={translate(
|
||||
'auto.components.sidebar.SidebarFilter.detachedHead',
|
||||
'Hide detached HEAD'
|
||||
)}
|
||||
checked={hideDetachedHeadWorkspaces}
|
||||
onChange={setHideDetachedHeadWorkspaces}
|
||||
/>
|
||||
|
||||
{canFilterRepos && (
|
||||
<>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
<FilterToggleRow
|
||||
icon={<GitCommitHorizontal className="size-3.5" />}
|
||||
label={translate(
|
||||
'auto.components.sidebar.SidebarWorkspaceFilterSection.detachedHead',
|
||||
'Hide detached HEAD'
|
||||
)}
|
||||
checked={hideDetachedHeadWorkspaces}
|
||||
onChange={setHideDetachedHeadWorkspaces}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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'}`
|
||||
|
||||
@@ -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
|
||||
])
|
||||
|
||||
@@ -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<typeof sidebarHasActiveFilters>[0]
|
||||
|
||||
function filterState(overrides: Partial<FilterState> = {}): 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
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
@@ -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> = {}): 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> = {}): VisibleOptions
|
||||
}
|
||||
}
|
||||
|
||||
type FilterState = Parameters<typeof sidebarHasActiveFilters>[0]
|
||||
|
||||
function filterState(overrides: Partial<FilterState> = {}): 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
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<string, Repo>
|
||||
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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 アクティビティのレイアウト",
|
||||
|
||||
@@ -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 활동 레이아웃",
|
||||
|
||||
@@ -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": "智能体活动布局",
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, boolean>
|
||||
setShowDotfilesForWorktree: (worktreeId: string, showDotfiles: boolean) => void
|
||||
toggleShowDotfilesForWorktree: (worktreeId: string) => void
|
||||
@@ -2033,6 +2035,8 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (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<AppState, [], [], UISlice> = (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:
|
||||
|
||||
@@ -460,6 +460,7 @@ export function getDefaultUIState(): PersistedUIState {
|
||||
hideDefaultBranchWorkspace: false,
|
||||
hideAutomationGeneratedWorkspaces: false,
|
||||
hideCliCreatedWorkspaces: false,
|
||||
hideDetachedHeadWorkspaces: false,
|
||||
showDotfilesByWorktree: {},
|
||||
filterRepoIds: [],
|
||||
collapsedGroups: [],
|
||||
|
||||
@@ -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<string, boolean>
|
||||
filterRepoIds: string[]
|
||||
|
||||
Reference in New Issue
Block a user