This commit is contained in:
Neil
2026-06-10 19:28:23 -07:00
committed by GitHub
parent d4bee7db5c
commit 05bf2bdd8e
4 changed files with 206 additions and 26 deletions
@@ -273,6 +273,17 @@ describe('SidebarNav', () => {
expect(mocks.updateSettings).toHaveBeenCalledWith({ showMobileButton: false })
})
it('hides task source shortcuts until the Tasks row is hovered or focused', async () => {
const container = await renderSidebarNav()
const tasksButton = getButtonByText(container, 'Tasks')
const shortcuts = tasksButton.querySelector('[aria-label="Open GitHub tasks"]')?.parentElement
expect(shortcuts?.className).toContain('hidden')
expect(shortcuts?.className).toContain('group-hover:flex')
expect(shortcuts?.className).toContain('group-focus-within:flex')
})
it('hides available Tasks from its sidebar context menu', async () => {
const container = await renderSidebarNav()
@@ -51,7 +51,8 @@ function HideSidebarMenu({ onHide }: { onHide: () => void }): React.JSX.Element
<ContextMenuContent>
<ContextMenuItem onSelect={onHide}>
<EyeOff className="size-3.5" />
{translate("auto.components.sidebar.SidebarNav.d599269755", "Hide from sidebar")}</ContextMenuItem>
{translate('auto.components.sidebar.SidebarNav.d599269755', 'Hide from sidebar')}
</ContextMenuItem>
</ContextMenuContent>
)
}
@@ -237,7 +238,7 @@ const SidebarNav = React.memo(function SidebarNav() {
aria-current={tasksActive ? 'page' : undefined}
data-contextual-tour-target="sidebar-tasks"
className={cn(
'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-[13px] font-medium tracking-tight transition-colors',
'group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-[13px] font-medium tracking-tight transition-colors',
tasksActive
? 'bg-worktree-sidebar-accent text-worktree-sidebar-accent-foreground'
: 'text-worktree-sidebar-foreground/60 hover:bg-worktree-sidebar-foreground/8',
@@ -251,12 +252,17 @@ const SidebarNav = React.memo(function SidebarNav() {
)}
strokeWidth={tasksActive ? 2.25 : 1.75}
/>
<span className="flex-1">{translate("auto.components.sidebar.SidebarNav.fee535205b", "Tasks")}</span>
<span className="flex items-center gap-1">
<span className="flex-1">
{translate('auto.components.sidebar.SidebarNav.fee535205b', 'Tasks')}
</span>
<span className="hidden items-center gap-1 group-hover:flex group-focus-within:flex">
{visibleTaskProviders.includes('github') ? (
<TaskProviderShortcut
canBrowseTasks={canBrowseTasks}
label={translate("auto.components.sidebar.SidebarNav.0ccba862b8", "Open GitHub tasks")}
label={translate(
'auto.components.sidebar.SidebarNav.0ccba862b8',
'Open GitHub tasks'
)}
onOpen={() => {
openTaskPage({ taskSource: 'github' })
}}
@@ -267,7 +273,10 @@ const SidebarNav = React.memo(function SidebarNav() {
{visibleTaskProviders.includes('gitlab') ? (
<TaskProviderShortcut
canBrowseTasks={canBrowseTasks}
label={translate("auto.components.sidebar.SidebarNav.196c1b5362", "Open GitLab tasks")}
label={translate(
'auto.components.sidebar.SidebarNav.196c1b5362',
'Open GitLab tasks'
)}
onOpen={() => {
openTaskPage({ taskSource: 'gitlab' })
}}
@@ -278,7 +287,10 @@ const SidebarNav = React.memo(function SidebarNav() {
{visibleTaskProviders.includes('linear') ? (
<TaskProviderShortcut
canBrowseTasks={canBrowseTasks}
label={translate("auto.components.sidebar.SidebarNav.c39ab10000", "Open Linear tasks")}
label={translate(
'auto.components.sidebar.SidebarNav.c39ab10000',
'Open Linear tasks'
)}
onOpen={() => {
openTaskPage({ taskSource: 'linear' })
}}
@@ -289,7 +301,10 @@ const SidebarNav = React.memo(function SidebarNav() {
{visibleTaskProviders.includes('jira') ? (
<TaskProviderShortcut
canBrowseTasks={canBrowseTasks}
label={translate("auto.components.sidebar.SidebarNav.e7ad3c540d", "Open Jira tasks")}
label={translate(
'auto.components.sidebar.SidebarNav.e7ad3c540d',
'Open Jira tasks'
)}
onOpen={() => {
openTaskPage({ taskSource: 'jira' })
}}
@@ -324,7 +339,9 @@ const SidebarNav = React.memo(function SidebarNav() {
)}
strokeWidth={automationsActive ? 2.25 : 1.75}
/>
<span className="flex-1">{translate("auto.components.sidebar.SidebarNav.f323383e9a", "Automations")}</span>
<span className="flex-1">
{translate('auto.components.sidebar.SidebarNav.f323383e9a', 'Automations')}
</span>
</button>
</ContextMenuTrigger>
<HideSidebarMenu onHide={hideAutomationsButton} />
@@ -349,7 +366,9 @@ const SidebarNav = React.memo(function SidebarNav() {
)}
strokeWidth={activityActive ? 2.25 : 1.75}
/>
<span className="flex-1">{translate("auto.components.sidebar.SidebarNav.9c95e1ce91", "Agents")}</span>
<span className="flex-1">
{translate('auto.components.sidebar.SidebarNav.9c95e1ce91', 'Agents')}
</span>
{activityUnreadCount > 0 ? (
<span className="rounded-full bg-primary px-1.5 py-px text-[10px] font-semibold text-primary-foreground">
{activityUnreadCount}
@@ -381,10 +400,13 @@ const SidebarNav = React.memo(function SidebarNav() {
)}
strokeWidth={mobileActive ? 2.25 : 1.75}
/>
<span className="flex-1">{translate("auto.components.sidebar.SidebarNav.1b5c41caee", "Orca Mobile")}</span>
<span className="flex-1">
{translate('auto.components.sidebar.SidebarNav.1b5c41caee', 'Orca Mobile')}
</span>
{mobileOnboardingBadge.visible ? (
<span className="rounded-full bg-primary px-1.5 py-px text-[10px] font-semibold text-primary-foreground">
{translate("auto.components.sidebar.SidebarNav.c86d83b5c3", "New")}</span>
{translate('auto.components.sidebar.SidebarNav.c86d83b5c3', 'New')}
</span>
) : null}
</button>
</ContextMenuTrigger>
@@ -394,14 +416,19 @@ const SidebarNav = React.memo(function SidebarNav() {
<button
type="button"
onClick={() => openModal('worktree-palette')}
aria-label={translate("auto.components.sidebar.SidebarNav.0c3395fd32", "Search worktrees and browser tabs")}
aria-label={translate(
'auto.components.sidebar.SidebarNav.0c3395fd32',
'Search worktrees and browser tabs'
)}
className="group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-[13px] font-medium tracking-tight text-worktree-sidebar-foreground/60 transition-colors hover:bg-worktree-sidebar-foreground/8"
>
<Search
className="size-4 shrink-0 text-worktree-sidebar-foreground/30"
strokeWidth={1.75}
/>
<span className="flex-1">{translate("auto.components.sidebar.SidebarNav.80611a8b10", "Search")}</span>
<span className="flex-1">
{translate('auto.components.sidebar.SidebarNav.80611a8b10', 'Search')}
</span>
<kbd className="hidden rounded border border-border/60 bg-background/40 px-1.5 py-px font-mono text-[10px] font-medium text-muted-foreground group-hover:inline-flex items-center">
{worktreePaletteShortcut}
</kbd>
@@ -4,12 +4,19 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { SidebarSettingsHelpMenu } from './SidebarSettingsHelpMenu'
const mocks = vi.hoisted(() => ({
openModal: vi.fn(),
openSettingsPage: vi.fn(),
openSettingsTarget: vi.fn(),
appRestart: vi.fn(),
updaterCheck: vi.fn(),
shellOpenUrl: vi.fn(),
useShortcutLabel: vi.fn()
useShortcutLabel: vi.fn(),
setupProgress: {
ready: true,
coreDoneCount: 2,
coreTotal: 5,
stepDone: {}
}
}))
let updateStatus = { state: 'idle' } as const
@@ -17,6 +24,7 @@ let updateStatus = { state: 'idle' } as const
vi.mock('@/store', () => ({
useAppStore: (selector: (state: unknown) => unknown) =>
selector({
openModal: mocks.openModal,
openSettingsPage: mocks.openSettingsPage,
openSettingsTarget: mocks.openSettingsTarget,
updateStatus
@@ -31,6 +39,18 @@ vi.mock('@/hooks/useMountedRef', () => ({
useMountedRef: () => ({ current: true })
}))
vi.mock('../onboarding/show-onboarding-event', () => ({
showOnboardingFromRenderer: vi.fn()
}))
vi.mock('../setup-guide/use-setup-guide-progress', () => ({
useSetupGuideProgress: () => mocks.setupProgress
}))
vi.mock('../setup-guide/SetupGuideProgressRing', () => ({
SetupGuideProgressRing: () => <span data-testid="setup-guide-progress-ring" />
}))
vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenu: ({ children }: { children: ReactNode }) => <>{children}</>,
DropdownMenuContent: ({ children }: { children: ReactNode }) => <>{children}</>,
@@ -79,10 +99,15 @@ describe('SidebarSettingsHelpMenu', () => {
vi.clearAllMocks()
mocks.useShortcutLabel.mockReturnValue('⌘,')
updateStatus = { state: 'idle' }
mocks.setupProgress = {
ready: true,
coreDoneCount: 2,
coreTotal: 5,
stepDone: {}
}
})
it('renders the help button with correct aria-label', () => {
updateStatus = { state: 'idle' }
const html = renderToStaticMarkup(<SidebarSettingsHelpMenu />)
expect(html).toContain('Help')
})
@@ -102,6 +127,28 @@ describe('SidebarSettingsHelpMenu', () => {
expect(html).toContain('Keyboard Shortcuts')
})
it('renders Milestones with progress when setup is incomplete', () => {
const html = renderToStaticMarkup(<SidebarSettingsHelpMenu />)
expect(html).toContain('Milestones')
expect(html).toContain('data-testid="setup-guide-progress-ring"')
})
it('hides Milestones when setup is complete', () => {
mocks.setupProgress = {
ready: true,
coreDoneCount: 5,
coreTotal: 5,
stepDone: {}
}
const html = renderToStaticMarkup(<SidebarSettingsHelpMenu />)
expect(html).not.toContain('Milestones')
})
it('hides the Onboarding admin entry by default', () => {
const html = renderToStaticMarkup(<SidebarSettingsHelpMenu />)
expect(html).not.toContain('Onboarding')
})
it('renders Docs link', () => {
const html = renderToStaticMarkup(<SidebarSettingsHelpMenu />)
expect(html).toContain('Docs')
@@ -122,6 +169,11 @@ describe('SidebarSettingsHelpMenu', () => {
expect(html).toContain('Discord')
})
it('renders X link', () => {
const html = renderToStaticMarkup(<SidebarSettingsHelpMenu />)
expect(html).toContain('>X<')
})
it('renders Check for Updates menu item', () => {
const html = renderToStaticMarkup(<SidebarSettingsHelpMenu />)
expect(html).toContain('Check for Updates')
@@ -1,14 +1,20 @@
import React, { useState } from 'react'
import {
BookOpen,
CircleHelp,
ExternalLink,
Github,
Keyboard,
Loader2,
MessageSquareText,
RefreshCw,
RotateCw,
School,
ScrollText,
Settings
} from 'lucide-react'
import { toast } from 'sonner'
import logo from '../../../../../resources/logo.svg'
import { useAppStore } from '@/store'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
@@ -21,6 +27,9 @@ import {
} from '@/components/ui/dropdown-menu'
import { useMountedRef } from '@/hooks/useMountedRef'
import { useShortcutLabel } from '@/hooks/useShortcutLabel'
import { showOnboardingFromRenderer } from '../onboarding/show-onboarding-event'
import { SetupGuideProgressRing } from '../setup-guide/SetupGuideProgressRing'
import { useSetupGuideProgress } from '../setup-guide/use-setup-guide-progress'
import { SidebarFeedbackDialog } from './SidebarFeedbackDialog'
import { translate } from '@/i18n/i18n'
@@ -28,6 +37,7 @@ const DOCS_URL = 'https://www.onorca.dev/docs'
const CHANGELOG_URL = 'https://onorca.dev/changelog'
const GITHUB_URL = 'https://github.com/stablyai/orca'
const DISCORD_URL = 'https://discord.gg/fzjDKHxv8Q'
const X_URL = 'https://x.com/orca_build'
function openExternalUrl(url: string): void {
void window.api.shell.openUrl(url)
@@ -41,9 +51,26 @@ function DiscordIcon(): React.JSX.Element {
)
}
function ExternalMenuItem({ label, url }: { label: string; url: string }): React.JSX.Element {
function XIcon(): React.JSX.Element {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" className="size-3.5 fill-current">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
)
}
function ExternalMenuItem({
label,
url,
icon
}: {
label: string
url: string
icon: React.ReactNode
}): React.JSX.Element {
return (
<DropdownMenuItem onSelect={() => openExternalUrl(url)}>
{icon}
{label}
<ExternalLink className="ml-auto size-3 text-muted-foreground" />
</DropdownMenuItem>
@@ -51,17 +78,23 @@ function ExternalMenuItem({ label, url }: { label: string; url: string }): React
}
export function SidebarSettingsHelpMenu(): React.JSX.Element {
const openModal = useAppStore((s) => s.openModal)
const openSettingsPage = useAppStore((s) => s.openSettingsPage)
const openSettingsTarget = useAppStore((s) => s.openSettingsTarget)
const updateStatus = useAppStore((s) => s.updateStatus)
const setupProgress = useSetupGuideProgress(true, false, false)
const settingsShortcut = useShortcutLabel('app.settings')
const [menuOpen, setMenuOpen] = useState(false)
const [feedbackOpen, setFeedbackOpen] = useState(false)
const [showAdminOptions, setShowAdminOptions] = useState(false)
const [isRestartingOrca, setIsRestartingOrca] = useState(false)
const lastShowOnboardingAtRef = React.useRef(0)
const mountedRef = useMountedRef()
const showMilestones =
setupProgress.ready && setupProgress.coreDoneCount < setupProgress.coreTotal
const handleMenuOpenChange = (open: boolean): void => {
setMenuOpen(open)
if (!open) {
@@ -70,9 +103,20 @@ export function SidebarSettingsHelpMenu(): React.JSX.Element {
}
const revealAdminOptions = (altKey: boolean): void => {
// Why: onboarding replay and restart stay off the default Help menu; holding
// Option/Alt before opening is an intentional power-user affordance.
setShowAdminOptions(altKey)
}
const handleShowOnboarding = (): void => {
const now = Date.now()
if (now - lastShowOnboardingAtRef.current < 500) {
return
}
lastShowOnboardingAtRef.current = now
void showOnboardingFromRenderer()
}
const handleRestartOrca = (): void => {
if (isRestartingOrca) {
return
@@ -107,6 +151,10 @@ export function SidebarSettingsHelpMenu(): React.JSX.Element {
void window.api.updater.check({ includePrerelease: shiftKey })
}
const openMilestones = (): void => {
openModal('setup-guide', { telemetrySource: 'help_menu' })
}
return (
<>
<DropdownMenu modal={false} open={menuOpen} onOpenChange={handleMenuOpenChange}>
@@ -133,12 +181,20 @@ export function SidebarSettingsHelpMenu(): React.JSX.Element {
{translate('auto.components.sidebar.SidebarSettingsHelpMenu.2991a0106c', 'Help')}
</TooltipContent>
</Tooltip>
<DropdownMenuContent side="top" align="start" sideOffset={8} className="w-48">
<DropdownMenuContent side="top" align="start" sideOffset={8} className="w-52">
<DropdownMenuItem onSelect={openSettingsPage}>
<Settings className="size-3.5" />
{translate('auto.components.sidebar.SidebarSettingsHelpMenu.a428c25998', 'Settings')}
<span className="ml-auto text-xs tracking-wide opacity-60">{settingsShortcut}</span>
</DropdownMenuItem>
<DropdownMenuItem onSelect={openShortcutsSettings}>
<Keyboard className="size-3.5" />
{translate(
'auto.components.sidebar.SidebarSettingsHelpMenu.e565171a7c',
'Keyboard Shortcuts'
)}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={() => setFeedbackOpen(true)}>
<MessageSquareText className="size-3.5" />
{translate(
@@ -146,17 +202,43 @@ export function SidebarSettingsHelpMenu(): React.JSX.Element {
'Send Feedback'
)}
</DropdownMenuItem>
<DropdownMenuItem onSelect={openShortcutsSettings}>
<ExternalLink className="size-3.5" />
{translate(
'auto.components.sidebar.SidebarSettingsHelpMenu.e565171a7c',
'Keyboard Shortcuts'
)}
</DropdownMenuItem>
<DropdownMenuSeparator />
{showMilestones ? (
<DropdownMenuItem onSelect={openMilestones}>
<img
src={logo}
alt=""
aria-hidden="true"
className="size-3.5 object-contain invert opacity-55 dark:invert-0"
/>
{translate(
'auto.components.sidebar.SidebarSettingsHelpMenu.f8a2c91d4e',
'Milestones'
)}
<SetupGuideProgressRing
done={setupProgress.coreDoneCount}
total={setupProgress.coreTotal}
sizeClassName="size-4"
className="ml-auto"
/>
</DropdownMenuItem>
) : null}
{showAdminOptions ? (
<DropdownMenuItem
className="whitespace-nowrap"
onClick={handleShowOnboarding}
onSelect={handleShowOnboarding}
>
<School className="size-3.5" />
{translate(
'auto.components.sidebar.SidebarSettingsHelpMenu.b7e4d2a19c',
'Onboarding'
)}
</DropdownMenuItem>
) : null}
<ExternalMenuItem
label={translate('auto.components.sidebar.SidebarSettingsHelpMenu.cdc87f897e', 'Docs')}
url={DOCS_URL}
icon={<BookOpen className="size-3.5" />}
/>
<ExternalMenuItem
label={translate(
@@ -164,19 +246,27 @@ export function SidebarSettingsHelpMenu(): React.JSX.Element {
'Changelog'
)}
url={CHANGELOG_URL}
icon={<ScrollText className="size-3.5" />}
/>
<DropdownMenuSeparator />
<ExternalMenuItem
label={translate(
'auto.components.sidebar.SidebarSettingsHelpMenu.5687ab246a',
'GitHub'
)}
url={GITHUB_URL}
icon={<Github className="size-3.5" />}
/>
<DropdownMenuItem onSelect={() => openExternalUrl(DISCORD_URL)}>
<DiscordIcon />
{translate('auto.components.sidebar.SidebarSettingsHelpMenu.eb9884e55b', 'Discord')}
<ExternalLink className="ml-auto size-3 text-muted-foreground" />
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => openExternalUrl(X_URL)}>
<XIcon />
{translate('auto.components.sidebar.SidebarSettingsHelpMenu.c4f8e1b72a', 'X')}
<ExternalLink className="ml-auto size-3 text-muted-foreground" />
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
disabled={updateStatus.state === 'checking' || updateStatus.state === 'downloading'}