Fix right sidebar peek visibility and drag handling

This commit is contained in:
Brennan Benson
2026-07-16 15:31:02 -07:00
parent 724247475b
commit 8b4b749bb3
17 changed files with 230 additions and 31 deletions
@@ -25,6 +25,7 @@ import { useActiveWorktree, useRepoById } from '@/store/selectors'
import { useChecksPanelTerminalWorktree } from './use-checks-panel-terminal-worktree'
import { cn } from '@/lib/utils'
import { openHttpLink } from '@/lib/http-link-routing'
import { isRightSidebarRevealed } from '@/lib/right-sidebar-visibility'
import { Button } from '@/components/ui/button'
import { DetachedHeadBadge } from '@/components/DetachedHeadBadge'
import {
@@ -374,9 +375,9 @@ export default function ChecksPanel(): React.JSX.Element {
// Why: the sidebar stays mounted when closed (for performance). Gate
// polling on visibility so we don't fetch checks/comments — or poll the
// terminal cwd — in the background when the panel isn't visible.
const rightSidebarOpen = useAppStore((s) => s.rightSidebarOpen)
const rightSidebarRevealed = useAppStore(isRightSidebarRevealed)
const rightSidebarTab = useAppStore((s) => s.rightSidebarTab)
const isPanelVisible = rightSidebarOpen && rightSidebarTab === 'checks'
const isPanelVisible = rightSidebarRevealed && rightSidebarTab === 'checks'
// Follow the active terminal's cwd so linked-PR/checks state tracks the
// worktree the terminal is actually operating in (e.g. across a stack),
@@ -55,6 +55,7 @@ import { CLOSE_ALL_CONTEXT_MENUS_EVENT } from '@/components/tab-bar/SortableTab'
import type { RightSidebarExplorerView } from '../../../../shared/types'
import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner'
import { createNewTerminalTab } from '@/components/terminal/terminal-tab-actions'
import { isRightSidebarRevealed } from '@/lib/right-sidebar-visibility'
function FileExplorerFiles(): React.JSX.Element {
const explorerView = useAppStore((s) => s.rightSidebarExplorerView)
@@ -97,7 +98,7 @@ function FileExplorerFiles(): React.JSX.Element {
const openFiles = useAppStore((s) => s.openFiles)
const closeFile = useAppStore((s) => s.closeFile)
const openModal = useAppStore((s) => s.openModal)
const rightSidebarOpen = useAppStore((s) => s.rightSidebarOpen)
const rightSidebarRevealed = useAppStore(isRightSidebarRevealed)
const showDotfiles = useAppStore((s) =>
activeWorktreeId ? (s.showDotfilesByWorktree[activeWorktreeId] ?? true) : true
)
@@ -119,7 +120,7 @@ function FileExplorerFiles(): React.JSX.Element {
const isFilesViewActive = explorerView === 'files'
const visibleFilesWorktreePath = getVisibleFileExplorerWorktreePath({
explorerView,
rightSidebarOpen,
rightSidebarOpen: rightSidebarRevealed,
worktreePath
})
const repoName = activeRepo?.displayName ?? (worktreePath ? basename(worktreePath) : '')
@@ -41,6 +41,7 @@ import { detectLanguage } from '@/lib/language-detect'
import { basename, dirname, joinPath } from '@/lib/path'
import { cn } from '@/lib/utils'
import { WORKSPACE_FILE_PATH_MIME } from '@/lib/workspace-file-drag'
import { isRightSidebarRevealed } from '@/lib/right-sidebar-visibility'
import { isFolderRepo } from '../../../../shared/repo-kind'
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip'
import { Button } from '@/components/ui/button'
@@ -1228,12 +1229,12 @@ function SourceControlInner(): React.JSX.Element {
recordKey: activePullRequestGenerationKey,
record: activePullRequestGenerationRecord
})
const rightSidebarOpen = useAppStore((s) => s.rightSidebarOpen)
// Why: gate polling on both the active tab AND the sidebar being open.
const rightSidebarRevealed = useAppStore(isRightSidebarRevealed)
// Why: gate polling on both the active tab AND the sidebar being revealed.
// The sidebar now stays mounted when closed (for performance), so without
// this guard the branchCompare interval and PR fetch would keep running
// with no visible consumer, wasting git process spawns and API calls.
const isBranchVisible = rightSidebarTab === 'source-control' && rightSidebarOpen
const isBranchVisible = rightSidebarTab === 'source-control' && rightSidebarRevealed
const refreshActiveGitStatus = useCallback(async (): Promise<void> => {
if (!activeWorktreeId || !worktreePath || isFolder) {
@@ -69,6 +69,41 @@ describe('RightSidebarEdgePeekZone', () => {
expect(useAppStore.getState().rightSidebarPeek).toBe(false)
})
it('does not arm while a mouse button is pressed at the edge', () => {
render(<RightSidebarEdgePeekZone />)
act(() => {
fireEvent.mouseMove(window, {
clientX: RIGHT_EDGE_X,
clientY: BELOW_TITLEBAR_Y,
buttons: 1
})
})
vi.advanceTimersByTime(PEEK_OPEN_DELAY_MS)
expect(useAppStore.getState().rightSidebarPeek).toBe(false)
})
it('cancels an armed peek when a scrollbar drag starts', () => {
render(<RightSidebarEdgePeekZone />)
moveMouse(RIGHT_EDGE_X, BELOW_TITLEBAR_Y)
fireEvent.mouseDown(window, { buttons: 1 })
vi.advanceTimersByTime(PEEK_OPEN_DELAY_MS)
expect(useAppStore.getState().rightSidebarPeek).toBe(false)
})
it('cancels an armed peek when the window loses focus', () => {
render(<RightSidebarEdgePeekZone />)
moveMouse(RIGHT_EDGE_X, BELOW_TITLEBAR_Y)
fireEvent.blur(window)
vi.advanceTimersByTime(PEEK_OPEN_DELAY_MS)
expect(useAppStore.getState().rightSidebarPeek).toBe(false)
})
it('ignores the top titlebar zone so window controls stay clickable', () => {
render(<RightSidebarEdgePeekZone />)
@@ -55,6 +55,12 @@ export function RightSidebarEdgePeekZone(): React.JSX.Element | null {
}
}
const onMouseMove = (event: MouseEvent): void => {
// Why: scrollbar and resize drags can remain in the edge band longer
// than the hover delay; pressed buttons must never arm an overlay.
if (event.buttons !== 0) {
clearOpenTimer()
return
}
// Why: exclude the top 36px titlebar row so the peek never arms over
// window controls (Windows/Linux custom chrome).
const inEdgeBand =
@@ -72,9 +78,13 @@ export function RightSidebarEdgePeekZone(): React.JSX.Element | null {
}
}
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mousedown', clearOpenTimer)
window.addEventListener('blur', clearOpenTimer)
return () => {
clearOpenTimer()
window.removeEventListener('mousemove', onMouseMove)
window.removeEventListener('mousedown', clearOpenTimer)
window.removeEventListener('blur', clearOpenTimer)
}
}, [edgePeekEnabled, rightSidebarOpen, rightSidebarPeek, setRightSidebarPeek])
@@ -20,6 +20,7 @@ import {
import { keybindingMatchesAction } from '../../../../shared/keybindings'
import { translate } from '@/i18n/i18n'
import { isEditableTarget } from '@/lib/editable-target'
import { isRightSidebarRevealed } from '@/lib/right-sidebar-visibility'
export function shouldIgnoreFileExplorerKeyTarget(target: EventTarget | null): boolean {
return (
@@ -52,7 +53,7 @@ export function useFileExplorerKeys(opts: {
scrollToIndex: (index: number) => void
activeWorktreeId: string | null
}): void {
const rightSidebarOpen = useAppStore((s) => s.rightSidebarOpen)
const rightSidebarRevealed = useAppStore(isRightSidebarRevealed)
const rightSidebarTab = useAppStore((s) => s.rightSidebarTab)
const rightSidebarExplorerView = useAppStore((s) => s.rightSidebarExplorerView)
const keybindings = useAppStore((s) => s.keybindings)
@@ -139,7 +140,7 @@ export function useFileExplorerKeys(opts: {
const onKeyDown = (e: KeyboardEvent): void => {
if (
!rightSidebarOpen ||
!rightSidebarRevealed ||
rightSidebarTab !== 'explorer' ||
rightSidebarExplorerView !== 'files'
) {
@@ -290,5 +291,11 @@ export function useFileExplorerKeys(opts: {
window.addEventListener('keydown', onKeyDown, { capture: true })
return () => window.removeEventListener('keydown', onKeyDown, { capture: true })
}, [keybindings, rightSidebarExplorerView, rightSidebarOpen, rightSidebarTab, opts.containerRef])
}, [
keybindings,
rightSidebarExplorerView,
rightSidebarRevealed,
rightSidebarTab,
opts.containerRef
])
}
@@ -15,6 +15,7 @@ type PollState = {
gitConflictOperationByWorktree: Record<string, unknown>
sshConnectionStates: Map<string, { status: string }>
rightSidebarOpen?: boolean
rightSidebarPeek?: boolean
rightSidebarTab?: string
rightSidebarExplorerView?: string
openFiles?: unknown[]
@@ -254,6 +255,27 @@ describe('useGitStatusPolling', () => {
expect(globalThis.setInterval).toHaveBeenCalledWith(expect.any(Function), 30_000)
})
it('uses the interactive git status cadence while the sidebar is peeked', async () => {
const { gitStatus } = await usePollingOnce(
{
entries: [],
conflictOperation: 'unknown',
head: 'abc123',
branch: 'refs/heads/main'
},
{
stateOverrides: {
rightSidebarOpen: false,
rightSidebarPeek: true,
openFiles: []
}
}
)
expect(gitStatus).toHaveBeenCalledTimes(1)
expect(globalThis.setInterval).toHaveBeenCalledWith(expect.any(Function), 3000)
})
it('does not install the visible git status poll while disabled', async () => {
const { state, gitStatus } = await usePollingOnce(
{
@@ -15,6 +15,7 @@ import {
import { getRightSidebarWorktreeRuntimeSettings } from './file-explorer-runtime-owner'
import { useGitStatusFileWatchRefresh } from './git-status-file-watch-refresh'
import { useGitStatusPushSignalRefresh } from './git-status-push-signal-refresh'
import { isRightSidebarRevealed } from '@/lib/right-sidebar-visibility'
const MIN_STATUS_REFRESH_INTERVAL_MS = 3000
const INTERACTIVE_STATUS_POLL_INTERVAL_MS = MIN_STATUS_REFRESH_INTERVAL_MS
@@ -36,7 +37,7 @@ export function useGitStatusPolling(options: { enabled?: boolean } = {}): void {
const setConflictOperation = useAppStore((s) => s.setConflictOperation)
const conflictOperationByWorktree = useAppStore((s) => s.gitConflictOperationByWorktree)
const sshConnectionStates = useAppStore((s) => s.sshConnectionStates)
const rightSidebarOpen = useAppStore((s) => s.rightSidebarOpen)
const rightSidebarRevealed = useAppStore(isRightSidebarRevealed)
const rightSidebarTab = useAppStore((s) => s.rightSidebarTab)
const rightSidebarExplorerView = useAppStore((s) => s.rightSidebarExplorerView)
const openFiles = useAppStore((s) => s.openFiles)
@@ -56,7 +57,7 @@ export function useGitStatusPolling(options: { enabled?: boolean } = {}): void {
const activeGitStatusPollingArgs = {
activeWorktreeId,
worktreePath,
rightSidebarOpen,
rightSidebarOpen: rightSidebarRevealed,
rightSidebarTab,
rightSidebarExplorerView,
openFiles
@@ -179,7 +180,7 @@ export function useGitStatusPolling(options: { enabled?: boolean } = {}): void {
isConnectionReady,
openFiles,
rightSidebarExplorerView,
rightSidebarOpen,
rightSidebarOpen: rightSidebarRevealed,
rightSidebarTab,
worktreePath
})
@@ -59,6 +59,7 @@ describe('getEditorExternalWatchTargets', () => {
activeWorktreeId?: string | null
runtimeEnvironmentId?: string | null
rightSidebarOpen?: boolean
rightSidebarPeek?: boolean
rightSidebarTab?: EditorExternalWatchTargetState['rightSidebarTab']
rightSidebarExplorerView?: EditorExternalWatchTargetState['rightSidebarExplorerView']
gitStatusHugeByWorktree?: EditorExternalWatchTargetState['gitStatusHugeByWorktree']
@@ -69,6 +70,7 @@ describe('getEditorExternalWatchTargets', () => {
repos: [args.repo],
activeWorktreeId: args.activeWorktreeId ?? null,
rightSidebarOpen: args.rightSidebarOpen ?? false,
rightSidebarPeek: args.rightSidebarPeek ?? false,
rightSidebarTab: args.rightSidebarTab ?? 'explorer',
rightSidebarExplorerView: args.rightSidebarExplorerView ?? 'files',
gitStatusHugeByWorktree: args.gitStatusHugeByWorktree ?? {},
@@ -136,6 +138,29 @@ describe('getEditorExternalWatchTargets', () => {
])
})
it('keeps watching the active worktree while the file explorer is peeked', () => {
const repo = makeRepo('repo-active-peeked')
const worktree = makeWorktree(repo.id, 'wt-active-peeked')
expect(
getEditorExternalWatchTargets(
makeState({
repo,
worktree,
activeWorktreeId: worktree.id,
rightSidebarPeek: true
})
).targets
).toEqual([
{
worktreeId: 'wt-active-peeked',
worktreePath: '/repo-active-peeked/worktree',
connectionId: undefined,
runtimeEnvironmentId: null
}
])
})
it('does not watch the active worktree while Explorer search is visible', () => {
const repo = makeRepo('repo-active-search')
const worktree = makeWorktree(repo.id, 'wt-active-search')
@@ -30,6 +30,7 @@ import {
} from './worktree-file-change-event'
import { isGitRepoKind } from '../../../shared/repo-kind'
import { markFileChangedOnDisk } from '@/components/editor/editor-changed-on-disk-mark'
import { isRightSidebarRevealed } from '@/lib/right-sidebar-visibility'
// Why: atomic-write patterns (Claude Code's Edit tool, editors like vim,
// VSCode) land as a short burst of `update` events — or `delete + create` on
@@ -96,6 +97,7 @@ export type EditorExternalWatchTargetState = Pick<
| 'activeWorktreeId'
| 'settings'
| 'rightSidebarOpen'
| 'rightSidebarPeek'
| 'rightSidebarTab'
| 'rightSidebarExplorerView'
| 'gitStatusHugeByWorktree'
@@ -107,7 +109,7 @@ let cachedWorktreesByRepo: AppState['worktreesByRepo'] | null = null
let cachedRepos: AppState['repos'] | null = null
let cachedActiveWorktreeId: string | null = null
let cachedRuntimeEnvironmentId: string | undefined
let cachedRightSidebarOpen: boolean | null = null
let cachedRightSidebarRevealed: boolean | null = null
let cachedRightSidebarTab: AppState['rightSidebarTab'] | null = null
let cachedRightSidebarExplorerView: AppState['rightSidebarExplorerView'] | null = null
let cachedGitStatusHugeByWorktree: AppState['gitStatusHugeByWorktree'] | null = null
@@ -129,13 +131,14 @@ export function getEditorExternalWatchTargets(
state: EditorExternalWatchTargetState
): WatchedTargetsSnapshot {
const runtimeEnvironmentId = state.settings?.activeRuntimeEnvironmentId?.trim() || undefined
const rightSidebarRevealed = isRightSidebarRevealed(state)
if (
cachedOpenFiles === state.openFiles &&
cachedWorktreesByRepo === state.worktreesByRepo &&
cachedRepos === state.repos &&
cachedActiveWorktreeId === state.activeWorktreeId &&
cachedRuntimeEnvironmentId === runtimeEnvironmentId &&
cachedRightSidebarOpen === state.rightSidebarOpen &&
cachedRightSidebarRevealed === rightSidebarRevealed &&
cachedRightSidebarTab === state.rightSidebarTab &&
cachedRightSidebarExplorerView === state.rightSidebarExplorerView &&
cachedGitStatusHugeByWorktree === state.gitStatusHugeByWorktree &&
@@ -175,7 +178,7 @@ export function getEditorExternalWatchTargets(
state.sshConnectionStates.get(activeRepo.connectionId)?.status === 'connected')
const activeWorktreeNeedsSidebarWatch =
activeWorktreeId !== null &&
state.rightSidebarOpen &&
rightSidebarRevealed &&
((state.rightSidebarTab === 'explorer' && state.rightSidebarExplorerView === 'files') ||
(state.rightSidebarTab === 'source-control' && sourceControlCanConsumeWatch))
if (activeWorktreeNeedsSidebarWatch) {
@@ -222,7 +225,7 @@ export function getEditorExternalWatchTargets(
cachedRepos = state.repos
cachedActiveWorktreeId = state.activeWorktreeId
cachedRuntimeEnvironmentId = runtimeEnvironmentId
cachedRightSidebarOpen = state.rightSidebarOpen
cachedRightSidebarRevealed = rightSidebarRevealed
cachedRightSidebarTab = state.rightSidebarTab
cachedRightSidebarExplorerView = state.rightSidebarExplorerView
cachedGitStatusHugeByWorktree = state.gitStatusHugeByWorktree
+16 -2
View File
@@ -123,7 +123,8 @@
"fe21e8f6f5": "Go back ({{value0}})",
"064bd07810": "Go back",
"ce37cf5279": "Toggle sidebar ({{value0}})",
"e4b9e7dff7": "Toggle sidebar"
"e4b9e7dff7": "Toggle sidebar",
"rightSidebarEdgePeekHint": "Hover the right edge to peek"
},
"web": {
"WebConnect": {
@@ -7120,6 +7121,10 @@
"cardLayout": "card layout",
"workspaceOptions": "workspace options",
"detailed": "detailed"
},
"rightSidebarEdgePeek": {
"title": "Right Sidebar Edge Peek",
"description": "Hover the window edge to temporarily reveal a closed right sidebar."
}
}
},
@@ -9540,7 +9545,8 @@
"fc3095d2ed": "explorer",
"aiVaultSessionHistory": "Agents",
"folderWorkspaces": "Attached worktrees",
"parentPrChecks": "PR Checks"
"parentPrChecks": "PR Checks",
"edgePeekHint": "Hover the right edge to peek"
},
"right": {
"panel": {
@@ -9925,6 +9931,14 @@
"archivedJumpUnavailable": "This session is in an archived worktree.",
"noActiveWorktreeMatch": "No active worktree matches this session.",
"noActiveWorktreeTarget": "No active worktree is available."
},
"edge": {
"peek": {
"tip": {
"title": "Hover the right edge to peek",
"description": "The right sidebar slides over the editor without reflowing it. Click the toggle or use the shortcut to pin it open. Change this anytime in Settings › Appearance."
}
}
}
}
},
+17 -3
View File
@@ -123,7 +123,8 @@
"fe21e8f6f5": "Volver ({{value0}})",
"064bd07810": "Volver",
"ce37cf5279": "Alternar barra lateral ({{value0}})",
"e4b9e7dff7": "Alternar barra lateral"
"e4b9e7dff7": "Alternar barra lateral",
"rightSidebarEdgePeekHint": "Hover the right edge to peek"
},
"web": {
"WebConnect": {
@@ -7083,7 +7084,11 @@
"detailed": "detallado"
},
"9a115966d3": "Minimizar a la bandeja al cerrar",
"4d5b9427b5": "Cuando está activado, cerrar la ventana mantiene Orca en ejecución en la bandeja del sistema en lugar de salir."
"4d5b9427b5": "Cuando está activado, cerrar la ventana mantiene Orca en ejecución en la bandeja del sistema en lugar de salir.",
"rightSidebarEdgePeek": {
"title": "Right Sidebar Edge Peek",
"description": "Hover the window edge to temporarily reveal a closed right sidebar."
}
}
},
"auto": {
@@ -9540,7 +9545,8 @@
"fc3095d2ed": "explorador",
"aiVaultSessionHistory": "Agentes",
"folderWorkspaces": "Worktrees adjuntos",
"parentPrChecks": "Checks de PR"
"parentPrChecks": "Checks de PR",
"edgePeekHint": "Hover the right edge to peek"
},
"right": {
"panel": {
@@ -9925,6 +9931,14 @@
"archivedJumpUnavailable": "Esta sesión está en un worktree archivado.",
"noActiveWorktreeMatch": "Ningún worktree activo coincide con esta sesión.",
"noActiveWorktreeTarget": "No hay ningún worktree activo disponible."
},
"edge": {
"peek": {
"tip": {
"title": "Hover the right edge to peek",
"description": "The right sidebar slides over the editor without reflowing it. Click the toggle or use the shortcut to pin it open. Change this anytime in Settings › Appearance."
}
}
}
}
},
+17 -3
View File
@@ -123,7 +123,8 @@
"fe21e8f6f5": "戻る ({{value0}})",
"064bd07810": "戻る",
"ce37cf5279": "サイドバーの切り替え ({{value0}})",
"e4b9e7dff7": "サイドバーの切り替え"
"e4b9e7dff7": "サイドバーの切り替え",
"rightSidebarEdgePeekHint": "Hover the right edge to peek"
},
"web": {
"WebConnect": {
@@ -7105,7 +7106,11 @@
"detailed": "詳細"
},
"9a115966d3": "閉じるときにトレイへ最小化",
"4d5b9427b5": "有効にすると、ウィンドウを閉じてもOrcaは終了せず、システムトレイで実行を続けます。"
"4d5b9427b5": "有効にすると、ウィンドウを閉じてもOrcaは終了せず、システムトレイで実行を続けます。",
"rightSidebarEdgePeek": {
"title": "Right Sidebar Edge Peek",
"description": "Hover the window edge to temporarily reveal a closed right sidebar."
}
}
},
"auto": {
@@ -9540,7 +9545,8 @@
"fc3095d2ed": "エクスプローラ",
"aiVaultSessionHistory": "エージェント",
"folderWorkspaces": "添付ワークツリー",
"parentPrChecks": "PR チェック"
"parentPrChecks": "PR チェック",
"edgePeekHint": "Hover the right edge to peek"
},
"right": {
"panel": {
@@ -9925,6 +9931,14 @@
"archivedJumpUnavailable": "このセッションはアーカイブされたワークツリーにあります。",
"noActiveWorktreeMatch": "このセッションに一致するアクティブなワークツリーはありません。",
"noActiveWorktreeTarget": "利用可能なアクティブなワークツリーはありません。"
},
"edge": {
"peek": {
"tip": {
"title": "Hover the right edge to peek",
"description": "The right sidebar slides over the editor without reflowing it. Click the toggle or use the shortcut to pin it open. Change this anytime in Settings › Appearance."
}
}
}
}
},
+16 -2
View File
@@ -123,7 +123,8 @@
"fe21e8f6f5": "돌아가기({{value0}})",
"064bd07810": "돌아가기",
"ce37cf5279": "사이드바 표시/숨기기({{value0}})",
"e4b9e7dff7": "사이드바 표시/숨기기"
"e4b9e7dff7": "사이드바 표시/숨기기",
"rightSidebarEdgePeekHint": "Hover the right edge to peek"
},
"web": {
"WebConnect": {
@@ -7068,6 +7069,10 @@
"cardLayout": "카드 레이아웃",
"workspaceOptions": "워크스페이스 옵션",
"detailed": "상세"
},
"rightSidebarEdgePeek": {
"title": "Right Sidebar Edge Peek",
"description": "Hover the window edge to temporarily reveal a closed right sidebar."
}
}
},
@@ -9540,7 +9545,8 @@
"fc3095d2ed": "탐색기",
"aiVaultSessionHistory": "에이전트",
"folderWorkspaces": "연결된 워크트리",
"parentPrChecks": "PR 검사"
"parentPrChecks": "PR 검사",
"edgePeekHint": "Hover the right edge to peek"
},
"right": {
"panel": {
@@ -9925,6 +9931,14 @@
"archivedJumpUnavailable": "이 세션은 보관된 작업 트리에 있습니다.",
"noActiveWorktreeMatch": "이 세션과 일치하는 활성 작업 트리가 없습니다.",
"noActiveWorktreeTarget": "사용 가능한 활성 작업 트리가 없습니다."
},
"edge": {
"peek": {
"tip": {
"title": "Hover the right edge to peek",
"description": "The right sidebar slides over the editor without reflowing it. Click the toggle or use the shortcut to pin it open. Change this anytime in Settings › Appearance."
}
}
}
}
},
+17 -3
View File
@@ -123,7 +123,8 @@
"fe21e8f6f5": "返回 ({{value0}})",
"064bd07810": "返回",
"ce37cf5279": "切换侧边栏 ({{value0}})",
"e4b9e7dff7": "切换侧边栏"
"e4b9e7dff7": "切换侧边栏",
"rightSidebarEdgePeekHint": "Hover the right edge to peek"
},
"web": {
"WebConnect": {
@@ -7068,7 +7069,11 @@
"detailed": "详细"
},
"9a115966d3": "关闭时最小化到托盘",
"4d5b9427b5": "启用后,关闭窗口会让 Orca 继续在系统托盘中运行,而不是退出。"
"4d5b9427b5": "启用后,关闭窗口会让 Orca 继续在系统托盘中运行,而不是退出。",
"rightSidebarEdgePeek": {
"title": "Right Sidebar Edge Peek",
"description": "Hover the window edge to temporarily reveal a closed right sidebar."
}
}
},
"auto": {
@@ -9540,7 +9545,8 @@
"fc3095d2ed": "资源管理器",
"aiVaultSessionHistory": "智能体",
"folderWorkspaces": "已附加的工作树",
"parentPrChecks": "PR 检查"
"parentPrChecks": "PR 检查",
"edgePeekHint": "Hover the right edge to peek"
},
"right": {
"panel": {
@@ -9925,6 +9931,14 @@
"archivedJumpUnavailable": "此会话位于已归档的工作树中。",
"noActiveWorktreeMatch": "没有与此会话匹配的活动工作树。",
"noActiveWorktreeTarget": "没有可用的活动工作树。"
},
"edge": {
"peek": {
"tip": {
"title": "Hover the right edge to peek",
"description": "The right sidebar slides over the editor without reflowing it. Click the toggle or use the shortcut to pin it open. Change this anytime in Settings › Appearance."
}
}
}
}
},
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import {
canShowRightSidebarForView,
isRightSidebarRevealed,
rightSidebarShowsPullRequestData
} from './right-sidebar-visibility'
import type { AppState } from '@/store/types'
@@ -22,6 +23,7 @@ function makeState(
}
],
rightSidebarOpen: true,
rightSidebarPeek: false,
rightSidebarTab: 'checks',
worktreesByRepo: { 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] },
...overrides
@@ -47,6 +49,12 @@ describe('right sidebar visibility helpers', () => {
expect(canShowRightSidebarForView('terminal')).toBe(true)
})
it('treats both pinned and peeked sidebars as revealed', () => {
expect(isRightSidebarRevealed({ rightSidebarOpen: true, rightSidebarPeek: false })).toBe(true)
expect(isRightSidebarRevealed({ rightSidebarOpen: false, rightSidebarPeek: true })).toBe(true)
expect(isRightSidebarRevealed({ rightSidebarOpen: false, rightSidebarPeek: false })).toBe(false)
})
it('does not treat hidden full-page sidebars as visible PR panels', () => {
expect(rightSidebarShowsPullRequestData(makeState({ activeView: 'tasks' }))).toBe(false)
})
@@ -79,4 +87,12 @@ describe('right sidebar visibility helpers', () => {
)
).toBe(true)
})
it('detects PR panels revealed by an edge peek', () => {
expect(
rightSidebarShowsPullRequestData(
makeState({ rightSidebarOpen: false, rightSidebarPeek: true })
)
).toBe(true)
})
})
@@ -17,6 +17,12 @@ export function canShowRightSidebarForView(activeView: ActiveView): boolean {
return !RIGHT_SIDEBAR_SUPPRESSED_VIEWS.has(activeView)
}
export function isRightSidebarRevealed(
state: Pick<AppState, 'rightSidebarOpen' | 'rightSidebarPeek'>
): boolean {
return state.rightSidebarOpen || state.rightSidebarPeek
}
export function rightSidebarShowsPullRequestData(
state: Pick<
AppState,
@@ -24,13 +30,14 @@ export function rightSidebarShowsPullRequestData(
| 'activeWorktreeId'
| 'repos'
| 'rightSidebarOpen'
| 'rightSidebarPeek'
| 'rightSidebarTab'
| 'worktreesByRepo'
>
): boolean {
if (
!canShowRightSidebarForView(state.activeView) ||
!state.rightSidebarOpen ||
!isRightSidebarRevealed(state) ||
(state.rightSidebarTab !== 'checks' && state.rightSidebarTab !== 'source-control')
) {
return false