Improve current workspace reveal control (#2327)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson 2026-05-19 15:34:18 -07:00 committed by GitHub
parent 7d71131bb4
commit 288a8b423d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 1094 additions and 557 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,81 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const revealWorktreeInSidebar = vi.fn()
const baseState: {
activeWorktreeId: string | null
revealWorktreeInSidebar: typeof revealWorktreeInSidebar
worktreesByRepo: Record<string, ({ id: string; repoId: string } & Record<string, unknown>)[]>
} = {
activeWorktreeId: 'wt-1',
revealWorktreeInSidebar,
worktreesByRepo: {
'repo-1': [
{
id: 'wt-1',
repoId: 'repo-1',
path: '/repo/worktrees/one',
displayName: 'One',
branch: 'one',
head: 'abc',
isBare: false,
isMainWorktree: false,
comment: '',
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0
}
]
}
}
vi.mock('@/store', () => ({
useAppStore: {
getState: () => baseState
}
}))
describe('reveal-sidebar-worktree', () => {
beforeEach(() => {
revealWorktreeInSidebar.mockReset()
baseState.activeWorktreeId = 'wt-1'
})
it('reveals the current workspace with the requested behavior', async () => {
const { revealCurrentSidebarWorktree } = await import('./reveal-sidebar-worktree')
expect(revealCurrentSidebarWorktree({ behavior: 'smooth' })).toBe(true)
expect(revealWorktreeInSidebar).toHaveBeenCalledWith('wt-1', { behavior: 'smooth' })
})
it('can reveal a specific workspace without changing other sidebar state', async () => {
const { revealSidebarWorktree } = await import('./reveal-sidebar-worktree')
expect(revealSidebarWorktree('wt-1', { behavior: 'auto' })).toBe(true)
expect(revealWorktreeInSidebar).toHaveBeenCalledWith('wt-1', { behavior: 'auto' })
})
it('does not reveal a missing workspace', async () => {
const { revealSidebarWorktree } = await import('./reveal-sidebar-worktree')
expect(revealSidebarWorktree('missing', { behavior: 'auto' })).toBe(false)
expect(revealWorktreeInSidebar).not.toHaveBeenCalled()
})
it('does nothing when there is no active workspace', async () => {
const { revealCurrentSidebarWorktree } = await import('./reveal-sidebar-worktree')
baseState.activeWorktreeId = null
expect(revealCurrentSidebarWorktree({ behavior: 'smooth' })).toBe(false)
expect(revealWorktreeInSidebar).not.toHaveBeenCalled()
})
})

View File

@ -0,0 +1,24 @@
import { useAppStore } from '@/store'
import { findWorktreeById } from '@/store/slices/worktree-helpers'
export function revealSidebarWorktree(
worktreeId: string,
options?: { behavior?: 'auto' | 'smooth' }
): boolean {
const state = useAppStore.getState()
const worktree = findWorktreeById(state.worktreesByRepo, worktreeId)
if (!worktree) {
return false
}
state.revealWorktreeInSidebar(worktreeId, options)
return true
}
export function revealCurrentSidebarWorktree(options?: { behavior?: 'auto' | 'smooth' }): boolean {
const activeWorktreeId = useAppStore.getState().activeWorktreeId
if (!activeWorktreeId) {
return false
}
return revealSidebarWorktree(activeWorktreeId, options)
}

View File

@ -1,5 +1,11 @@
import { describe, expect, it } from 'vitest'
import { shouldAdjustWorktreeSidebarMeasuredRowScroll } from './WorktreeList'
import {
shouldConsumeStartupRevealForPendingReveal,
resolvePendingSidebarReveal,
shouldAdjustWorktreeSidebarMeasuredRowScroll,
shouldShowFloatingCurrentWorkspaceButton,
shouldQueueStartupSidebarReveal
} from './WorktreeList'
describe('shouldAdjustWorktreeSidebarMeasuredRowScroll', () => {
it('suppresses measured-row scroll correction while TanStack is scrolling', () => {
@ -31,4 +37,157 @@ describe('shouldAdjustWorktreeSidebarMeasuredRowScroll', () => {
})
).toBe(true)
})
it('queues a startup reveal once the active workspace and rows are ready', () => {
expect(
shouldQueueStartupSidebarReveal({
hasQueuedStartupReveal: false,
workspaceSessionReady: true,
persistedUIReady: true,
activeWorktreeId: 'wt-1',
pendingRevealWorktree: null,
renderRowCount: 3
})
).toBe(true)
})
it('does not queue a startup reveal when another reveal is already pending', () => {
expect(
shouldQueueStartupSidebarReveal({
hasQueuedStartupReveal: false,
workspaceSessionReady: true,
persistedUIReady: true,
activeWorktreeId: 'wt-1',
pendingRevealWorktree: { worktreeId: 'wt-2', behavior: 'smooth' },
renderRowCount: 3
})
).toBe(false)
})
it('consumes startup reveal when an explicit reveal is already pending', () => {
expect(
shouldConsumeStartupRevealForPendingReveal({
hasQueuedStartupReveal: false,
workspaceSessionReady: true,
persistedUIReady: true,
pendingRevealWorktree: { worktreeId: 'wt-2', behavior: 'smooth' }
})
).toBe(true)
})
it('does not consume startup reveal before hydration is ready', () => {
expect(
shouldConsumeStartupRevealForPendingReveal({
hasQueuedStartupReveal: false,
workspaceSessionReady: true,
persistedUIReady: false,
pendingRevealWorktree: { worktreeId: 'wt-2', behavior: 'smooth' }
})
).toBe(false)
})
it('does not queue a startup reveal twice', () => {
expect(
shouldQueueStartupSidebarReveal({
hasQueuedStartupReveal: true,
workspaceSessionReady: true,
persistedUIReady: true,
activeWorktreeId: 'wt-1',
pendingRevealWorktree: null,
renderRowCount: 3
})
).toBe(false)
})
it('does not queue a startup reveal before hydration is ready', () => {
expect(
shouldQueueStartupSidebarReveal({
hasQueuedStartupReveal: false,
workspaceSessionReady: false,
persistedUIReady: true,
activeWorktreeId: 'wt-1',
pendingRevealWorktree: null,
renderRowCount: 3
})
).toBe(false)
})
it('keeps pending reveal requests when the worktree still exists but the row is unresolved', () => {
expect(
resolvePendingSidebarReveal({
targetIndex: -1,
targetWorktreeStillExists: true
})
).toBe('keep-pending')
})
it('clears pending reveal requests once the target disappears', () => {
expect(
resolvePendingSidebarReveal({
targetIndex: -1,
targetWorktreeStillExists: false
})
).toBe('clear')
})
it('scrolls and clears once the target row is resolvable', () => {
expect(
resolvePendingSidebarReveal({
targetIndex: 4,
targetWorktreeStillExists: true
})
).toBe('scroll-and-clear')
})
it('shows the floating reveal action when the current workspace row is hidden', () => {
expect(
shouldShowFloatingCurrentWorkspaceButton({
currentWorktreeId: 'wt-1',
currentRowIndex: -1,
currentItem: null,
scrollTop: 0,
viewportHeight: 400,
pendingRevealWorktreeId: null
})
).toBe(true)
})
it('shows the floating reveal action when the current workspace is outside the scrollport', () => {
expect(
shouldShowFloatingCurrentWorkspaceButton({
currentWorktreeId: 'wt-1',
currentRowIndex: 10,
currentItem: { start: 500, end: 560 },
scrollTop: 0,
viewportHeight: 400,
pendingRevealWorktreeId: null
})
).toBe(true)
})
it('hides the floating reveal action when the current workspace is visible', () => {
expect(
shouldShowFloatingCurrentWorkspaceButton({
currentWorktreeId: 'wt-1',
currentRowIndex: 3,
currentItem: { start: 120, end: 180 },
scrollTop: 100,
viewportHeight: 200,
pendingRevealWorktreeId: null
})
).toBe(false)
})
it('hides the floating reveal action while the current workspace reveal is pending', () => {
expect(
shouldShowFloatingCurrentWorkspaceButton({
currentWorktreeId: 'wt-1',
currentRowIndex: -1,
currentItem: null,
scrollTop: 0,
viewportHeight: 400,
pendingRevealWorktreeId: 'wt-1'
})
).toBe(false)
})
})

View File

@ -47,6 +47,11 @@ import { DEFAULT_PET_ID, isBundledPetId } from '../../components/pet/pet-models'
import { revokeCustomPetBlobUrl } from '../../components/pet/pet-blob-cache'
import { isGitRepoKind } from '../../../../shared/repo-kind'
export type PendingSidebarWorktreeReveal = {
worktreeId: string
behavior: 'auto' | 'smooth'
}
function clampPetSize(size: number): number {
if (!Number.isFinite(size)) {
return PET_SIZE_DEFAULT
@ -408,8 +413,11 @@ export type UISlice = {
* problem. */
petSize: number
setPetSize: (size: number) => void
pendingRevealWorktreeId: string | null
revealWorktreeInSidebar: (worktreeId: string) => void
pendingRevealWorktree: PendingSidebarWorktreeReveal | null
revealWorktreeInSidebar: (
worktreeId: string,
options?: { behavior?: PendingSidebarWorktreeReveal['behavior'] }
) => void
clearPendingRevealWorktreeId: () => void
// Why: lets the SourceControl sidebar request that the diff editor scroll
// to a specific note. Cleared by the diff decorator after it reveals the
@ -924,9 +932,15 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (set, get)
return partial
}),
pendingRevealWorktreeId: null,
revealWorktreeInSidebar: (worktreeId) => set({ pendingRevealWorktreeId: worktreeId }),
clearPendingRevealWorktreeId: () => set({ pendingRevealWorktreeId: null }),
pendingRevealWorktree: null,
revealWorktreeInSidebar: (worktreeId, options) =>
set({
pendingRevealWorktree: {
worktreeId,
behavior: options?.behavior ?? 'smooth'
}
}),
clearPendingRevealWorktreeId: () => set({ pendingRevealWorktree: null }),
scrollToDiffCommentId: null,
setScrollToDiffCommentId: (id) => set({ scrollToDiffCommentId: id }),
persistedUIReady: false,