fix: new worktree not sorting to top with sort=recent (#314)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Neil 2026-04-05 14:47:41 -07:00 committed by GitHub
parent df43248761
commit 3ec7fcb05e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 45 additions and 3 deletions

View File

@ -354,4 +354,31 @@ describe('buildWorktreeComparator', () => {
expect(worktrees.map((worktree) => worktree.id)).toEqual(['active', 'background'])
})
it('ranks a just-created worktree above shutdown worktrees with passive signals', () => {
const justCreated = makeWorktree({
id: 'new',
displayName: 'New',
lastActivityAt: NOW
})
// Shutdown worktree with max passive signals but no recent activity
const shutdown = makeWorktree({
id: 'shutdown',
displayName: 'Shutdown',
isUnread: true,
linkedIssue: 42,
lastActivityAt: NOW - 2 * 24 * 60 * 60 * 1000
})
const prCache = {
'/tmp/repo-1::shutdown': {
data: { number: 17 },
fetchedAt: NOW
}
}
const worktrees = [shutdown, justCreated]
worktrees.sort(buildWorktreeComparator('recent', null, repoMap, prCache, NOW))
expect(worktrees.map((worktree) => worktree.id)).toEqual(['new', 'shutdown'])
})
})

View File

@ -73,7 +73,12 @@ function computeSmartScoreFromSignals(
const activityAge = now - (worktree.lastActivityAt || 0)
if (worktree.lastActivityAt > 0) {
const ONE_DAY = 24 * 60 * 60 * 1000
score += 24 * Math.max(0, 1 - activityAge / ONE_DAY)
// Why 36: a just-created worktree has only this signal (no live tab yet,
// since the PTY spawns asynchronously after creation). Weight must exceed
// the max passive-signal combination for shutdown worktrees
// (isUnread 18 + PR 10 + issue 6 = 34) so brand-new worktrees always
// appear at the top of the "recent" sort immediately.
score += 36 * Math.max(0, 1 - activityAge / ONE_DAY)
}
return score
@ -161,12 +166,12 @@ export function buildWorktreeComparator(
*
* Scoring:
* running AI job +60
* recent activity +36 (decays over 24 hours)
* needs attention +35
* unread +18
* open terminal +12
* live branch PR +10
* linked issue +6
* recent activity +24 (decays over 24 hours)
*/
export function computeSmartScore(
worktree: Worktree,

View File

@ -231,12 +231,22 @@ export const createTerminalSlice: StateCreator<AppState, [], [], TerminalSlice>
next[wId] = next[wId].map((t) => (t.id === tabId ? { ...t, ptyId } : t))
}
const existingPtyIds = s.ptyIdsByTabId[tabId] ?? []
// Why: when a brand-new tab in the active worktree receives its first
// PTY, the live-tab signal (+12) flips on. bumpWorktreeActivity (below)
// intentionally skips sortEpoch for the active worktree to prevent the
// reorder-on-click bug (PR #209), but that means the sort never sees
// the new signal. Bump sortEpoch here so a just-created worktree
// immediately reflects its live-tab score instead of waiting for an
// unrelated event to trigger a re-sort.
const isFirstPty = existingPtyIds.length === 0
const isActiveWorktree = worktreeId != null && s.activeWorktreeId === worktreeId
return {
tabsByWorktree: next,
ptyIdsByTabId: {
...s.ptyIdsByTabId,
[tabId]: existingPtyIds.includes(ptyId) ? existingPtyIds : [...existingPtyIds, ptyId]
}
},
...(isFirstPty && isActiveWorktree ? { sortEpoch: s.sortEpoch + 1 } : {})
}
})