Files
orca/src/shared/external-worktree-visibility.ts
T
Brennan Benson 83e2123582 Add global worktree visibility source defaults (#14276)
* Add global external worktree visibility defaults

* Expand global worktree visibility source defaults

* Fix host-scoped visibility settings races

* Fix global worktree visibility integration

* Enable source visibility defaults on mobile

* Polish external worktree settings navigation

* Clarify inherited worktree visibility settings

* feat(sidebar): replace the inherited-visibility switch with a Show/Hide picker

Each source row now shows a two-segment Show / Hide control preselected to the
global setting, and explains itself only where the project actually disagrees:
an "Overriding global setting: <value>" card names the value being ignored.
Picking the segment global already holds drops the override instead of pinning
a duplicate, so the same control both overrides and reverts, retiring the
separate "Use global" link. The dialog footer now lists every inheritable
source with its global value.

* fix(sidebar): preserve reset for matching visibility overrides
2026-08-14 12:15:58 -07:00

140 lines
4.4 KiB
TypeScript

import { normalizeRuntimePathSeparators } from './cross-platform-path'
import {
normalizeCustomWorktreeVisibilitySources,
normalizeWorktreeVisibilitySourcePreferences
} from './worktree/visibility-sources'
import type { WorktreeVisibilityDefaults } from './global-settings-types'
import type { ExternalWorktreeVisibility, Repo } from './repo-types'
export const EXTERNAL_WORKTREE_VISIBILITY_ROLLOUT_AT = Date.UTC(2026, 4, 23)
export const UNKNOWN_EXTERNAL_WORKTREE_PARENT_PATH = 'Unknown location'
function trimRuntimePathTrailingSlash(value: string): string {
if (value === '/' || /^[A-Za-z]:\/$/.test(value)) {
return value
}
return value.replace(/\/+$/, '')
}
export function getExternalWorktreeParentPath(worktreePath: string | undefined): string {
if (!worktreePath) {
return UNKNOWN_EXTERNAL_WORKTREE_PARENT_PATH
}
const normalized = trimRuntimePathTrailingSlash(normalizeRuntimePathSeparators(worktreePath))
if (!normalized) {
return UNKNOWN_EXTERNAL_WORKTREE_PARENT_PATH
}
if (normalized.startsWith('//')) {
const parts = normalized.slice(2).split('/').filter(Boolean)
if (parts.length < 2) {
return UNKNOWN_EXTERNAL_WORKTREE_PARENT_PATH
}
if (parts.length === 2) {
return `//${parts[0]}/${parts[1]}`
}
return `//${parts.slice(0, -1).join('/')}`
}
const lastSeparatorIndex = normalized.lastIndexOf('/')
if (lastSeparatorIndex === -1) {
return UNKNOWN_EXTERNAL_WORKTREE_PARENT_PATH
}
if (lastSeparatorIndex === 0) {
return '/'
}
if (/^[A-Za-z]:\/$/.test(normalized)) {
return normalized
}
if (/^[A-Za-z]:\/[^/]+$/.test(normalized)) {
return `${normalized.slice(0, 2)}/`
}
return normalized.slice(0, lastSeparatorIndex)
}
export function isLegacyRepoForExternalWorktreeVisibility(repo: Repo): boolean {
if (typeof repo.externalWorktreeVisibilityLegacy === 'boolean') {
return repo.externalWorktreeVisibilityLegacy
}
if (repo.externalWorktreeVisibility === undefined) {
return true
}
if (!Number.isFinite(repo.addedAt)) {
return true
}
return repo.addedAt < EXTERNAL_WORKTREE_VISIBILITY_ROLLOUT_AT
}
export function effectiveExternalWorktreeVisibility(
repo: Pick<Repo, 'externalWorktreeVisibility'>,
isLegacyRepoForVisibility: boolean,
defaults?: WorktreeVisibilityDefaults
): ExternalWorktreeVisibility {
if (repo.externalWorktreeVisibility) {
return repo.externalWorktreeVisibility
}
if (defaults?.external === 'show' || defaults?.external === 'hide') {
return defaults.external
}
return isLegacyRepoForVisibility ? 'show' : 'hide'
}
export function normalizeWorktreeVisibilityDefaults(
value: unknown
): WorktreeVisibilityDefaults | undefined {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return undefined
}
const {
external,
customSources: rawCustomSources,
sourcePreferences: rawSourcePreferences,
...futureDefaults
} = value as WorktreeVisibilityDefaults & Record<string, unknown>
if (external !== 'show' && external !== 'hide') {
return undefined
}
const customSources = normalizeCustomWorktreeVisibilitySources(rawCustomSources)
const sourcePreferences = normalizeWorktreeVisibilitySourcePreferences(rawSourcePreferences)
return {
...futureDefaults,
external,
...(customSources !== undefined ? { customSources } : {}),
...(sourcePreferences !== undefined ? { sourcePreferences } : {})
}
}
export function migrateExternalWorktreeVisibilityDefaults(
repos: readonly Repo[],
value: unknown
): {
repos: Repo[]
defaults: WorktreeVisibilityDefaults
changed: boolean
} {
const defaults = normalizeWorktreeVisibilityDefaults(value)
if (defaults) {
return { repos: [...repos], defaults, changed: false }
}
const migratedRepos = repos.map((repo) => {
if (
repo.kind === 'folder' ||
repo.externalWorktreeVisibility !== undefined ||
!isLegacyRepoForExternalWorktreeVisibility(repo)
) {
return repo
}
return {
...repo,
externalWorktreeVisibility: 'show' as const,
externalWorktreeVisibilityLegacy: true
}
})
return { repos: migratedRepos, defaults: { external: 'hide' }, changed: true }
}
export function effectiveAgentWorktreeVisibility(
repo: Pick<Repo, 'agentWorktreeVisibility'>
): ExternalWorktreeVisibility {
// Why: scratch worktrees have always defaulted hidden, including for legacy repos.
return repo.agentWorktreeVisibility === 'show' ? 'show' : 'hide'
}