diff --git a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx index 0682c1b0790..9a6c264c2cf 100644 --- a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx +++ b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx @@ -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), diff --git a/src/renderer/src/components/right-sidebar/FileExplorer.tsx b/src/renderer/src/components/right-sidebar/FileExplorer.tsx index fb91c21963b..4ebb33fef6a 100644 --- a/src/renderer/src/components/right-sidebar/FileExplorer.tsx +++ b/src/renderer/src/components/right-sidebar/FileExplorer.tsx @@ -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) : '') diff --git a/src/renderer/src/components/right-sidebar/SourceControl.tsx b/src/renderer/src/components/right-sidebar/SourceControl.tsx index aef619545ea..742e38bc714 100644 --- a/src/renderer/src/components/right-sidebar/SourceControl.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControl.tsx @@ -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 => { if (!activeWorktreeId || !worktreePath || isFolder) { diff --git a/src/renderer/src/components/right-sidebar/right-sidebar-edge-peek.test.tsx b/src/renderer/src/components/right-sidebar/right-sidebar-edge-peek.test.tsx index a50b554564f..87b5987ce6c 100644 --- a/src/renderer/src/components/right-sidebar/right-sidebar-edge-peek.test.tsx +++ b/src/renderer/src/components/right-sidebar/right-sidebar-edge-peek.test.tsx @@ -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() + + 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() + + 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() + + 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() diff --git a/src/renderer/src/components/right-sidebar/right-sidebar-edge-peek.tsx b/src/renderer/src/components/right-sidebar/right-sidebar-edge-peek.tsx index 75adbdda394..3d8eea38675 100644 --- a/src/renderer/src/components/right-sidebar/right-sidebar-edge-peek.tsx +++ b/src/renderer/src/components/right-sidebar/right-sidebar-edge-peek.tsx @@ -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]) diff --git a/src/renderer/src/components/right-sidebar/useFileExplorerKeys.ts b/src/renderer/src/components/right-sidebar/useFileExplorerKeys.ts index 22b58fb568b..b1602d45b49 100644 --- a/src/renderer/src/components/right-sidebar/useFileExplorerKeys.ts +++ b/src/renderer/src/components/right-sidebar/useFileExplorerKeys.ts @@ -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 + ]) } diff --git a/src/renderer/src/components/right-sidebar/useGitStatusPolling.test.ts b/src/renderer/src/components/right-sidebar/useGitStatusPolling.test.ts index 5c4d57338a3..4f18cec85a2 100644 --- a/src/renderer/src/components/right-sidebar/useGitStatusPolling.test.ts +++ b/src/renderer/src/components/right-sidebar/useGitStatusPolling.test.ts @@ -15,6 +15,7 @@ type PollState = { gitConflictOperationByWorktree: Record sshConnectionStates: Map 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( { diff --git a/src/renderer/src/components/right-sidebar/useGitStatusPolling.ts b/src/renderer/src/components/right-sidebar/useGitStatusPolling.ts index 895b69012a1..70d64088a48 100644 --- a/src/renderer/src/components/right-sidebar/useGitStatusPolling.ts +++ b/src/renderer/src/components/right-sidebar/useGitStatusPolling.ts @@ -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 }) diff --git a/src/renderer/src/hooks/useEditorExternalWatch-targets.test.ts b/src/renderer/src/hooks/useEditorExternalWatch-targets.test.ts index 1e5f5e45e35..63bc4039f5b 100644 --- a/src/renderer/src/hooks/useEditorExternalWatch-targets.test.ts +++ b/src/renderer/src/hooks/useEditorExternalWatch-targets.test.ts @@ -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') diff --git a/src/renderer/src/hooks/useEditorExternalWatch.ts b/src/renderer/src/hooks/useEditorExternalWatch.ts index 9fd18202b98..ac9dd41c7fc 100644 --- a/src/renderer/src/hooks/useEditorExternalWatch.ts +++ b/src/renderer/src/hooks/useEditorExternalWatch.ts @@ -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 diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 570c20c6c54..3beec152fc5 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -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." + } + } } } }, diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 2543c3ce6d3..b3d76c89306 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -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." + } + } } } }, diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 9727ae4adc4..1d51fa74932 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -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." + } + } } } }, diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index bff86bf9af6..bbb6a2944f7 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -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." + } + } } } }, diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index a0b21163a78..7cf699cc518 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -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." + } + } } } }, diff --git a/src/renderer/src/lib/right-sidebar-visibility.test.ts b/src/renderer/src/lib/right-sidebar-visibility.test.ts index 333ba995496..6eb0b0d3694 100644 --- a/src/renderer/src/lib/right-sidebar-visibility.test.ts +++ b/src/renderer/src/lib/right-sidebar-visibility.test.ts @@ -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) + }) }) diff --git a/src/renderer/src/lib/right-sidebar-visibility.ts b/src/renderer/src/lib/right-sidebar-visibility.ts index 950f2fcb7fd..5eb71971694 100644 --- a/src/renderer/src/lib/right-sidebar-visibility.ts +++ b/src/renderer/src/lib/right-sidebar-visibility.ts @@ -17,6 +17,12 @@ export function canShowRightSidebarForView(activeView: ActiveView): boolean { return !RIGHT_SIDEBAR_SUPPRESSED_VIEWS.has(activeView) } +export function isRightSidebarRevealed( + state: Pick +): 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