Fix worktree sort crash when display name is missing (#6993)
This commit is contained in:
parent
151c567074
commit
6dafe8c870
|
|
@ -63,7 +63,7 @@ import type {
|
|||
WorkspaceStatusDefinition
|
||||
} from '../../../../shared/types'
|
||||
import { DEFAULT_SHOW_SLEEPING_WORKSPACES } from '../../../../shared/constants'
|
||||
import { buildWorktreeComparator } from './smart-sort'
|
||||
import { buildWorktreeComparator, compareWorktreeSortLabel } from './smart-sort'
|
||||
import {
|
||||
buildAttentionByWorktree,
|
||||
type SmartClass,
|
||||
|
|
@ -5193,7 +5193,7 @@ const WorktreeList = React.memo(function WorktreeList({
|
|||
sessionHasHadPty.current = true
|
||||
} else {
|
||||
nonArchivedWorktrees.sort(
|
||||
(a, b) => b.sortOrder - a.sortOrder || a.displayName.localeCompare(b.displayName)
|
||||
(a, b) => b.sortOrder - a.sortOrder || compareWorktreeSortLabel(a, b)
|
||||
)
|
||||
lastAttentionByWorktreeRef.current = null
|
||||
return nonArchivedWorktrees.map((w) => w.id)
|
||||
|
|
|
|||
|
|
@ -458,6 +458,22 @@ describe('sortWorktreesSmart — cold start fallback', () => {
|
|||
expect(sorted.map((w) => w.id)).toEqual(['b', 'a'])
|
||||
})
|
||||
|
||||
it('falls back to the path label when a persisted worktree has no displayName', () => {
|
||||
const missingDisplayName = {
|
||||
...makeWorktree({
|
||||
id: 'missing-display-name',
|
||||
path: '/tmp/alpha-path',
|
||||
sortOrder: 1
|
||||
}),
|
||||
displayName: undefined
|
||||
} as unknown as Worktree
|
||||
const named = makeWorktree({ id: 'named', displayName: 'Zulu', sortOrder: 1 })
|
||||
|
||||
const sorted = sortWorktreesSmart([named, missingDisplayName], {}, repoMap, {}, {}, {})
|
||||
|
||||
expect(sorted.map((w) => w.id)).toEqual(['missing-display-name', 'named'])
|
||||
})
|
||||
|
||||
it('treats slept tabs (tab.ptyId without live entry) as cold start', () => {
|
||||
// Why: tab.ptyId is the wake-hint sessionId preserved under sleep — not a
|
||||
// liveness signal. With slept tabs but no live PTYs, sortWorktreesSmart
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type {
|
|||
MigrationUnsupportedPtyEntry
|
||||
} from '../../../../shared/agent-status-types'
|
||||
import { tabHasLivePty } from '@/lib/tab-has-live-pty'
|
||||
import { basename } from '@/lib/path'
|
||||
import { IDLE, buildAttentionByWorktree, type WorktreeAttention } from './smart-attention'
|
||||
|
||||
export type SortBy = 'name' | 'smart' | 'recent' | 'repo' | 'manual'
|
||||
|
|
@ -40,6 +41,27 @@ export function effectiveRecentActivity(worktree: Worktree, now: number): number
|
|||
return Math.max(lastActivityAt, createdAt + CREATE_GRACE_MS)
|
||||
}
|
||||
|
||||
type WorktreeSortLabelInput = Pick<Worktree, 'displayName' | 'path' | 'id'>
|
||||
|
||||
export function getWorktreeSortLabel(worktree: WorktreeSortLabelInput): string {
|
||||
const displayName = typeof worktree.displayName === 'string' ? worktree.displayName.trim() : ''
|
||||
if (displayName) {
|
||||
return displayName
|
||||
}
|
||||
|
||||
// Why: persisted or remote worktree state can briefly omit displayName after
|
||||
// a custom workspace name is removed; sorting must stay render-safe.
|
||||
const pathLabel = typeof worktree.path === 'string' ? basename(worktree.path).trim() : ''
|
||||
return pathLabel || worktree.id
|
||||
}
|
||||
|
||||
export function compareWorktreeSortLabel(
|
||||
a: WorktreeSortLabelInput,
|
||||
b: WorktreeSortLabelInput
|
||||
): number {
|
||||
return getWorktreeSortLabel(a).localeCompare(getWorktreeSortLabel(b))
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a comparator for sorting worktrees based on the current sort mode.
|
||||
*
|
||||
|
|
@ -58,7 +80,7 @@ export function buildWorktreeComparator(
|
|||
return (a, b) => {
|
||||
switch (sortBy) {
|
||||
case 'name':
|
||||
return a.displayName.localeCompare(b.displayName)
|
||||
return compareWorktreeSortLabel(a, b)
|
||||
case 'smart': {
|
||||
const aw = attentionByWorktree.get(a.id) ?? IDLE
|
||||
const bw = attentionByWorktree.get(b.id) ?? IDLE
|
||||
|
|
@ -70,7 +92,7 @@ export function buildWorktreeComparator(
|
|||
// Why: idle worktrees fall through to recency (and the create-grace
|
||||
// floor for brand-new worktrees) before alphabetical.
|
||||
effectiveRecentActivity(b, now) - effectiveRecentActivity(a, now) ||
|
||||
a.displayName.localeCompare(b.displayName)
|
||||
compareWorktreeSortLabel(a, b)
|
||||
)
|
||||
}
|
||||
case 'recent':
|
||||
|
|
@ -87,13 +109,13 @@ export function buildWorktreeComparator(
|
|||
// events) and by meaningful meta edits (comment, isUnread).
|
||||
return (
|
||||
effectiveRecentActivity(b, now) - effectiveRecentActivity(a, now) ||
|
||||
a.displayName.localeCompare(b.displayName)
|
||||
compareWorktreeSortLabel(a, b)
|
||||
)
|
||||
case 'repo': {
|
||||
const ra = repoMap.get(a.repoId)?.displayName ?? ''
|
||||
const rb = repoMap.get(b.repoId)?.displayName ?? ''
|
||||
const cmp = ra.localeCompare(rb)
|
||||
return cmp !== 0 ? cmp : a.displayName.localeCompare(b.displayName)
|
||||
return cmp !== 0 ? cmp : compareWorktreeSortLabel(a, b)
|
||||
}
|
||||
case 'manual':
|
||||
// Why fallback to sortOrder: existing users have a persisted smart-sort
|
||||
|
|
@ -101,7 +123,7 @@ export function buildWorktreeComparator(
|
|||
// restored order instead of alphabetizing every legacy workspace.
|
||||
return (
|
||||
(b.manualOrder ?? b.sortOrder) - (a.manualOrder ?? a.sortOrder) ||
|
||||
a.displayName.localeCompare(b.displayName)
|
||||
compareWorktreeSortLabel(a, b)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -143,7 +165,7 @@ export function sortWorktreesSmart(
|
|||
// Cold start: use persisted sortOrder snapshot until the agent-status
|
||||
// snapshot lands and a warm sort runs.
|
||||
return [...worktrees].sort(
|
||||
(a, b) => b.sortOrder - a.sortOrder || a.displayName.localeCompare(b.displayName)
|
||||
(a, b) => b.sortOrder - a.sortOrder || compareWorktreeSortLabel(a, b)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue