mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 00:02:39 +00:00
Improve Settings setup guide entry (#4460)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import type { SettingsNavSection } from '@/lib/settings-navigation-types'
|
||||
import type { SettingsNavIcon, SettingsNavSection } from '@/lib/settings-navigation-types'
|
||||
import type { CmdJQuickAction } from './quick-actions'
|
||||
|
||||
export type CmdJSettingsResult = {
|
||||
@@ -7,7 +6,7 @@ export type CmdJSettingsResult = {
|
||||
kind: 'settings'
|
||||
title: string
|
||||
description: string
|
||||
icon: LucideIcon
|
||||
icon: SettingsNavIcon
|
||||
sectionId: string
|
||||
targetSectionId?: string
|
||||
order: number
|
||||
|
||||
@@ -224,7 +224,7 @@ export function FeatureWallSetupChecklist(
|
||||
const setupSteps = getFeatureWallSetupStepsForSection('setup')
|
||||
|
||||
return (
|
||||
<div className="grid h-full min-h-0 grid-rows-[auto_minmax(0,1fr)] gap-5 lg:grid-cols-[minmax(280px,360px)_minmax(0,1fr)] lg:grid-rows-[minmax(0,1fr)]">
|
||||
<div className="grid h-full min-h-0 grid-rows-[auto_minmax(0,1fr)] gap-5 lg:grid-cols-[minmax(200px,300px)_minmax(0,1fr)] lg:grid-rows-[minmax(0,1fr)]">
|
||||
<div className="scrollbar-sleek min-h-0 space-y-5 overflow-y-auto pr-1">
|
||||
<SetupSection
|
||||
title="Setup"
|
||||
|
||||
@@ -45,7 +45,7 @@ import { RuntimeEnvironmentsPane } from './RuntimeEnvironmentsPane'
|
||||
import { PrivacyPane } from './PrivacyPane'
|
||||
import { AdvancedPane } from './AdvancedPane'
|
||||
import { SettingsSidebar } from './SettingsSidebar'
|
||||
import { SettingsSetupGuideCard } from './SettingsSetupGuideCard'
|
||||
import { SettingsSetupGuidePane } from './SettingsSetupGuidePane'
|
||||
import { ActiveSettingsSectionProvider, SettingsSection } from './SettingsSection'
|
||||
import { matchesSettingsSearch } from './settings-search'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -857,7 +857,7 @@ function Settings(): React.JSX.Element {
|
||||
const generalNavGroups: SettingsNavGroup[] = SETTINGS_NAV_GROUPS.map((group) => ({
|
||||
...group,
|
||||
sections: generalNavSections.filter((section) => section.group === group.id)
|
||||
})).filter((group) => group.sections.length > 0)
|
||||
})).filter((group) => group.sections.length > 0 || group.id === 'setup')
|
||||
const repoNavSections = visibleNavSections
|
||||
.filter((section) => section.id.startsWith('repo-'))
|
||||
.map((section) => {
|
||||
@@ -910,8 +910,6 @@ function Settings(): React.JSX.Element {
|
||||
</div>
|
||||
) : (
|
||||
<ActiveSettingsSectionProvider value={activeSectionId}>
|
||||
{settingsSearchQuery.trim() === '' ? <SettingsSetupGuideCard /> : null}
|
||||
|
||||
<SettingsSection
|
||||
id="agents"
|
||||
title="Agents"
|
||||
@@ -982,6 +980,16 @@ function Settings(): React.JSX.Element {
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<SettingsSection
|
||||
id="setup-guide"
|
||||
title="Onboarding checklist"
|
||||
description="Finish the core workflows that make Orca useful for parallel agent work."
|
||||
searchEntries={getSectionSearchEntries('setup-guide')}
|
||||
bodyClassName="overflow-hidden p-0"
|
||||
>
|
||||
{isSectionMounted('setup-guide') ? <SettingsSetupGuidePane /> : null}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
id="general"
|
||||
title="General"
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { ArrowRight, ListChecks } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
import {
|
||||
getFeatureWallSetupSteps,
|
||||
getFirstIncompleteFeatureWallSetupStepId
|
||||
} from '../../../../shared/feature-wall-setup-steps'
|
||||
import type { FeatureWallSetupStepId } from '../../../../shared/feature-wall-setup-steps'
|
||||
import { useAppStore } from '@/store'
|
||||
import { Button } from '../ui/button'
|
||||
import { useSetupGuideProgress } from '../setup-guide/use-setup-guide-progress'
|
||||
|
||||
export function SettingsSetupGuideCard(): React.JSX.Element | null {
|
||||
const openModal = useAppStore((s) => s.openModal)
|
||||
const progress = useSetupGuideProgress(true, false, false)
|
||||
const setupSteps = useMemo(() => getFeatureWallSetupSteps(), [])
|
||||
const unfinishedSteps = useMemo(
|
||||
() => setupSteps.filter((step) => !progress.stepDone[step.id]),
|
||||
[progress.stepDone, setupSteps]
|
||||
)
|
||||
const completedStepCount = useMemo(
|
||||
() => setupSteps.filter((step) => progress.stepDone[step.id]).length,
|
||||
[progress.stepDone, setupSteps]
|
||||
)
|
||||
const firstUnfinishedStepId: FeatureWallSetupStepId = getFirstIncompleteFeatureWallSetupStepId(
|
||||
progress.stepDone
|
||||
)
|
||||
const setupComplete = unfinishedSteps.length === 0
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border-border/60 bg-card/50 px-5 py-4">
|
||||
<div className="flex items-start justify-between gap-5">
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<span className="flex size-9 shrink-0 items-center justify-center rounded-md border border-border bg-background text-muted-foreground">
|
||||
<ListChecks className="size-4" />
|
||||
</span>
|
||||
<div className="min-w-0 space-y-3">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-base font-semibold leading-tight text-foreground">
|
||||
Onboarding checklist
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{completedStepCount}/{setupSteps.length} setup tasks complete
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">
|
||||
{setupComplete ? 'Status' : 'To do'}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{setupComplete ? (
|
||||
<span className="rounded-full border border-border bg-background px-2.5 py-1 text-xs text-muted-foreground">
|
||||
All core workflows complete
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
{unfinishedSteps.slice(0, 3).map((step) => (
|
||||
<span
|
||||
key={step.id}
|
||||
className="rounded-full border border-border bg-background px-2.5 py-1 text-xs text-muted-foreground"
|
||||
>
|
||||
{step.name}
|
||||
</span>
|
||||
))}
|
||||
{unfinishedSteps.length > 3 ? (
|
||||
<span className="rounded-full border border-border bg-background px-2.5 py-1 text-xs text-muted-foreground">
|
||||
+{unfinishedSteps.length - 3} more
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="w-fit shrink-0 whitespace-nowrap gap-1.5"
|
||||
onClick={() =>
|
||||
openModal('setup-guide', {
|
||||
setupStepId: firstUnfinishedStepId,
|
||||
telemetrySource: 'settings'
|
||||
})
|
||||
}
|
||||
>
|
||||
{setupComplete ? 'Review setup' : 'Continue setup'}
|
||||
<ArrowRight className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
getFeatureWallSetupSteps,
|
||||
getFirstIncompleteFeatureWallSetupStepId
|
||||
} from '../../../../shared/feature-wall-setup-steps'
|
||||
import type { FeatureWallSetupStepId } from '../../../../shared/feature-wall-setup-steps'
|
||||
import { FeatureWallSetupChecklist } from '../feature-wall/FeatureWallSetupChecklist'
|
||||
import { useSetupGuideProgress } from '../setup-guide/use-setup-guide-progress'
|
||||
|
||||
export function SettingsSetupGuidePane(): React.JSX.Element {
|
||||
const setupSteps = useMemo(() => getFeatureWallSetupSteps(), [])
|
||||
const [activeStepId, setActiveStepId] = useState<FeatureWallSetupStepId>(
|
||||
() => setupSteps[0]?.id ?? 'default-agent'
|
||||
)
|
||||
const [userSelectedStep, setUserSelectedStep] = useState(false)
|
||||
const [orchestrationSkillInstalled, setOrchestrationSkillInstalled] = useState(false)
|
||||
const [browserUseSkillInstalled, setBrowserUseSkillInstalled] = useState(false)
|
||||
const progress = useSetupGuideProgress(
|
||||
true,
|
||||
orchestrationSkillInstalled,
|
||||
browserUseSkillInstalled
|
||||
)
|
||||
const activeStep = setupSteps.find((step) => step.id === activeStepId) ?? setupSteps[0] ?? null
|
||||
|
||||
useEffect(() => {
|
||||
if (userSelectedStep) {
|
||||
return
|
||||
}
|
||||
setActiveStepId(getFirstIncompleteFeatureWallSetupStepId(progress.stepDone))
|
||||
}, [progress.stepDone, userSelectedStep])
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeStep || userSelectedStep || !progress.stepDone[activeStep.id]) {
|
||||
return
|
||||
}
|
||||
const nextUnfinishedStepId = getFirstIncompleteFeatureWallSetupStepId(progress.stepDone)
|
||||
if (nextUnfinishedStepId !== activeStep.id) {
|
||||
setActiveStepId(nextUnfinishedStepId)
|
||||
}
|
||||
}, [activeStep, progress.stepDone, userSelectedStep])
|
||||
|
||||
const handleSelectStep = (id: FeatureWallSetupStepId): void => {
|
||||
setUserSelectedStep(true)
|
||||
setActiveStepId(id)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-[min(740px,calc(100vh-14rem))] min-h-[540px] p-5">
|
||||
<FeatureWallSetupChecklist
|
||||
activeStep={activeStep}
|
||||
progress={progress}
|
||||
onSelectStep={handleSelectStep}
|
||||
onOrchestrationSkillInstalledChange={setOrchestrationSkillInstalled}
|
||||
onBrowserUseSkillInstalledChange={setBrowserUseSkillInstalled}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { Bot, Mic, Network } from 'lucide-react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { SettingsSidebar } from './SettingsSidebar'
|
||||
import { TooltipProvider } from '../ui/tooltip'
|
||||
|
||||
vi.mock('@/hooks/useShortcutLabel', () => ({
|
||||
useShortcutLabel: () => '⌘F'
|
||||
@@ -9,52 +10,54 @@ vi.mock('@/hooks/useShortcutLabel', () => ({
|
||||
|
||||
function renderSidebar(): string {
|
||||
return renderToStaticMarkup(
|
||||
<SettingsSidebar
|
||||
activeSectionId="orchestration"
|
||||
generalGroups={[
|
||||
{
|
||||
id: 'capabilities',
|
||||
title: 'AI Capabilities',
|
||||
sections: [
|
||||
{
|
||||
id: 'agents',
|
||||
title: 'Agents',
|
||||
icon: Bot
|
||||
},
|
||||
{
|
||||
id: 'orchestration',
|
||||
title: 'Orchestration',
|
||||
icon: Network,
|
||||
installStatus: 'install'
|
||||
},
|
||||
{
|
||||
id: 'voice',
|
||||
title: 'Voice',
|
||||
icon: Mic,
|
||||
installStatus: 'installed'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'setup',
|
||||
title: 'Set Up',
|
||||
sections: [
|
||||
{
|
||||
id: 'accounts',
|
||||
title: 'AI Provider Accounts',
|
||||
icon: Bot,
|
||||
badge: 'Optional'
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
repoSections={[]}
|
||||
hasRepos={false}
|
||||
searchQuery=""
|
||||
onBack={vi.fn()}
|
||||
onSearchChange={vi.fn()}
|
||||
onSelectSection={vi.fn()}
|
||||
/>
|
||||
<TooltipProvider>
|
||||
<SettingsSidebar
|
||||
activeSectionId="orchestration"
|
||||
generalGroups={[
|
||||
{
|
||||
id: 'capabilities',
|
||||
title: 'AI Capabilities',
|
||||
sections: [
|
||||
{
|
||||
id: 'agents',
|
||||
title: 'Agents',
|
||||
icon: Bot
|
||||
},
|
||||
{
|
||||
id: 'orchestration',
|
||||
title: 'Orchestration',
|
||||
icon: Network,
|
||||
installStatus: 'install'
|
||||
},
|
||||
{
|
||||
id: 'voice',
|
||||
title: 'Voice',
|
||||
icon: Mic,
|
||||
installStatus: 'installed'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'setup',
|
||||
title: 'Set Up',
|
||||
sections: [
|
||||
{
|
||||
id: 'accounts',
|
||||
title: 'AI Provider Accounts',
|
||||
icon: Bot,
|
||||
badge: 'Optional'
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
repoSections={[]}
|
||||
hasRepos={false}
|
||||
searchQuery=""
|
||||
onBack={vi.fn()}
|
||||
onSearchChange={vi.fn()}
|
||||
onSelectSection={vi.fn()}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import type { RefObject } from 'react'
|
||||
import { ArrowLeft, Search, Server, type LucideIcon, type LucideProps } from 'lucide-react'
|
||||
import { ArrowLeft, Search, Server } from 'lucide-react'
|
||||
import logo from '../../../../../resources/logo.svg'
|
||||
import type { RepoIcon } from '../../../../shared/repo-icon'
|
||||
import type { SettingsNavInstallStatus } from '@/lib/settings-navigation-types'
|
||||
import type { SettingsNavIcon, SettingsNavInstallStatus } from '@/lib/settings-navigation-types'
|
||||
import { useShortcutLabel } from '@/hooks/useShortcutLabel'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { RepoIconGlyph } from '../repo/repo-icon'
|
||||
import { Button } from '../ui/button'
|
||||
import { Input } from '../ui/input'
|
||||
import { SetupGuideProgressRing } from '../setup-guide/SetupGuideProgressRing'
|
||||
import { useSettingsSetupGuideProgress } from './settings-setup-guide-progress'
|
||||
import type { SettingsSetupGuideProgress } from './settings-setup-guide-progress'
|
||||
|
||||
type NavSection = {
|
||||
id: string
|
||||
title: string
|
||||
icon: LucideIcon | ((props: LucideProps) => React.JSX.Element)
|
||||
icon: SettingsNavIcon
|
||||
badge?: string
|
||||
installStatus?: SettingsNavInstallStatus
|
||||
}
|
||||
@@ -48,6 +52,64 @@ type SettingsSidebarProps = {
|
||||
) => void
|
||||
}
|
||||
|
||||
type SettingsSetupGuideRowProps = {
|
||||
progress: SettingsSetupGuideProgress
|
||||
setupActive: boolean
|
||||
onSelect: (modifiers: {
|
||||
metaKey: boolean
|
||||
ctrlKey: boolean
|
||||
shiftKey: boolean
|
||||
altKey: boolean
|
||||
}) => void
|
||||
}
|
||||
|
||||
function SettingsSetupGuideNavRow({
|
||||
progress,
|
||||
setupActive,
|
||||
onSelect
|
||||
}: SettingsSetupGuideRowProps): React.JSX.Element {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-current={setupActive ? 'page' : undefined}
|
||||
aria-label={`Onboarding checklist, ${progress.doneCount} of ${progress.total} done. Show setup guide.`}
|
||||
onClick={(event) =>
|
||||
onSelect({
|
||||
metaKey: event.metaKey,
|
||||
ctrlKey: event.ctrlKey,
|
||||
shiftKey: event.shiftKey,
|
||||
altKey: event.altKey
|
||||
})
|
||||
}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50',
|
||||
setupActive
|
||||
? 'bg-accent font-medium text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-muted/60 hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={logo}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
'size-4 shrink-0 object-contain invert dark:invert-0',
|
||||
setupActive ? 'opacity-75' : 'opacity-45'
|
||||
)}
|
||||
/>
|
||||
<span className="flex min-w-0 flex-1 flex-col">
|
||||
<span className="truncate text-[13px] font-medium leading-4">Onboarding checklist</span>
|
||||
</span>
|
||||
<SetupGuideProgressRing
|
||||
done={progress.doneCount}
|
||||
total={progress.total}
|
||||
className="ml-auto shrink-0"
|
||||
tooltipLabel={`${progress.doneCount}/${progress.total} complete`}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function SettingsSidebar({
|
||||
activeSectionId,
|
||||
generalGroups,
|
||||
@@ -59,6 +121,9 @@ export function SettingsSidebar({
|
||||
onSearchChange,
|
||||
onSelectSection
|
||||
}: SettingsSidebarProps): React.JSX.Element {
|
||||
const setupGuideProgress = useSettingsSetupGuideProgress(true)
|
||||
const setupActive = activeSectionId === 'setup-guide'
|
||||
const showSetupGuideTopRow = setupGuideProgress.doneCount < setupGuideProgress.total
|
||||
const searchShortcutHint = useShortcutLabel('settings.search')
|
||||
const navItemClassName = (isActive: boolean): string =>
|
||||
cn(
|
||||
@@ -119,6 +184,16 @@ export function SettingsSidebar({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showSetupGuideTopRow ? (
|
||||
<div className="border-b border-border/50 px-3 py-3">
|
||||
<SettingsSetupGuideNavRow
|
||||
progress={setupGuideProgress}
|
||||
setupActive={setupActive}
|
||||
onSelect={(modifiers) => onSelectSection('setup-guide', modifiers)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-sleek px-3 py-4">
|
||||
<div className="space-y-5">
|
||||
{generalGroups.map((group) => (
|
||||
@@ -127,9 +202,11 @@ export function SettingsSidebar({
|
||||
{group.title}
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
{group.sections.map((section) => {
|
||||
const Icon = section.icon
|
||||
const isActive = activeSectionId === section.id
|
||||
{group.sections
|
||||
.filter((section) => section.id !== 'setup-guide')
|
||||
.map((section) => {
|
||||
const Icon = section.icon
|
||||
const isActive = activeSectionId === section.id
|
||||
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { FeatureWallSetupStepId } from '../../../../shared/feature-wall-setup-steps'
|
||||
import {
|
||||
getSettingsSetupGuideProgress,
|
||||
SETTINGS_SETUP_GUIDE_STEP_IDS
|
||||
} from './settings-setup-guide-progress'
|
||||
|
||||
describe('settings setup guide progress', () => {
|
||||
it('tracks the five-step settings checklist', () => {
|
||||
expect(SETTINGS_SETUP_GUIDE_STEP_IDS).toEqual([
|
||||
'split-terminal',
|
||||
'two-worktrees',
|
||||
'notifications',
|
||||
'default-agent',
|
||||
'task-sources'
|
||||
])
|
||||
})
|
||||
|
||||
it('returns a 3/5 progress label source and first incomplete step', () => {
|
||||
const stepDone = {
|
||||
'split-terminal': true,
|
||||
'two-worktrees': true,
|
||||
notifications: true
|
||||
} satisfies Partial<Record<FeatureWallSetupStepId, boolean>>
|
||||
|
||||
expect(getSettingsSetupGuideProgress(stepDone)).toEqual({
|
||||
doneCount: 3,
|
||||
total: 5,
|
||||
firstIncompleteStepId: 'default-agent'
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores setup-guide tasks that are not part of the settings checklist', () => {
|
||||
const stepDone = {
|
||||
'split-terminal': true,
|
||||
'two-worktrees': true,
|
||||
notifications: true,
|
||||
'setup-script': true,
|
||||
'add-two-repos': true,
|
||||
'agent-capabilities': true
|
||||
} satisfies Partial<Record<FeatureWallSetupStepId, boolean>>
|
||||
|
||||
expect(getSettingsSetupGuideProgress(stepDone)).toEqual({
|
||||
doneCount: 3,
|
||||
total: 5,
|
||||
firstIncompleteStepId: 'default-agent'
|
||||
})
|
||||
})
|
||||
|
||||
it('marks the settings checklist complete when all five settings steps are done', () => {
|
||||
const stepDone = Object.fromEntries(
|
||||
SETTINGS_SETUP_GUIDE_STEP_IDS.map((stepId) => [stepId, true])
|
||||
) as Record<FeatureWallSetupStepId, boolean>
|
||||
|
||||
expect(getSettingsSetupGuideProgress(stepDone)).toEqual({
|
||||
doneCount: 5,
|
||||
total: 5,
|
||||
firstIncompleteStepId: null
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import type { FeatureWallSetupStepId } from '../../../../shared/feature-wall-setup-steps'
|
||||
import { useAppStore } from '@/store'
|
||||
import { getFeatureWallSetupProgress } from '../feature-wall/feature-wall-setup-progress'
|
||||
|
||||
export const SETTINGS_SETUP_GUIDE_STEP_IDS = [
|
||||
'split-terminal',
|
||||
'two-worktrees',
|
||||
'notifications',
|
||||
'default-agent',
|
||||
'task-sources'
|
||||
] as const satisfies readonly FeatureWallSetupStepId[]
|
||||
|
||||
export type SettingsSetupGuideProgress = {
|
||||
doneCount: number
|
||||
total: number
|
||||
firstIncompleteStepId: FeatureWallSetupStepId | null
|
||||
}
|
||||
|
||||
export function getSettingsSetupGuideProgress(
|
||||
stepDone: Partial<Record<FeatureWallSetupStepId, boolean>>
|
||||
): SettingsSetupGuideProgress {
|
||||
const doneCount = SETTINGS_SETUP_GUIDE_STEP_IDS.filter((stepId) => stepDone[stepId]).length
|
||||
const firstIncompleteStepId =
|
||||
SETTINGS_SETUP_GUIDE_STEP_IDS.find((stepId) => !stepDone[stepId]) ?? null
|
||||
|
||||
return {
|
||||
doneCount,
|
||||
total: SETTINGS_SETUP_GUIDE_STEP_IDS.length,
|
||||
firstIncompleteStepId
|
||||
}
|
||||
}
|
||||
|
||||
export function useSettingsSetupGuideProgress(
|
||||
shouldRefreshTaskSourceState: boolean
|
||||
): SettingsSetupGuideProgress {
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const featureInteractions = useAppStore((s) => s.featureInteractions)
|
||||
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
|
||||
const tabsByWorktree = useAppStore((s) => s.tabsByWorktree)
|
||||
const terminalLayoutsByTabId = useAppStore((s) => s.terminalLayoutsByTabId)
|
||||
const preflightStatus = useAppStore((s) => s.preflightStatus)
|
||||
const preflightStatusChecked = useAppStore((s) => s.preflightStatusChecked)
|
||||
const refreshPreflightStatus = useAppStore((s) => s.refreshPreflightStatus)
|
||||
const linearStatus = useAppStore((s) => s.linearStatus)
|
||||
const linearStatusChecked = useAppStore((s) => s.linearStatusChecked)
|
||||
const checkLinearConnection = useAppStore((s) => s.checkLinearConnection)
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldRefreshTaskSourceState) {
|
||||
return
|
||||
}
|
||||
if (!preflightStatusChecked) {
|
||||
void refreshPreflightStatus()
|
||||
}
|
||||
if (!linearStatusChecked) {
|
||||
void checkLinearConnection()
|
||||
}
|
||||
}, [
|
||||
checkLinearConnection,
|
||||
linearStatusChecked,
|
||||
preflightStatusChecked,
|
||||
refreshPreflightStatus,
|
||||
shouldRefreshTaskSourceState
|
||||
])
|
||||
|
||||
const hasConnectedTaskSource =
|
||||
(preflightStatus?.gh.installed === true && preflightStatus.gh.authenticated === true) ||
|
||||
(preflightStatus?.glab?.installed === true && preflightStatus.glab.authenticated === true) ||
|
||||
linearStatus.connected === true
|
||||
|
||||
return useMemo(() => {
|
||||
// Why: Settings renders only five steps, so avoid the full setup guide
|
||||
// probes for setup scripts and agent skills, especially over SSH.
|
||||
const fullProgress = getFeatureWallSetupProgress({
|
||||
settings,
|
||||
featureInteractions,
|
||||
hasConnectedTaskSource,
|
||||
browserUseSkillInstalled: false,
|
||||
computerUseSkillInstalled: false,
|
||||
computerUsePermissionsReady: false,
|
||||
orchestrationSkillInstalled: false,
|
||||
gitRepoCount: 0,
|
||||
worktreesByRepo,
|
||||
tabsByWorktree,
|
||||
terminalLayoutsByTabId,
|
||||
hasSetupScript: false
|
||||
})
|
||||
return getSettingsSetupGuideProgress(fullProgress.stepDone)
|
||||
}, [
|
||||
featureInteractions,
|
||||
hasConnectedTaskSource,
|
||||
settings,
|
||||
tabsByWorktree,
|
||||
terminalLayoutsByTabId,
|
||||
worktreesByRepo
|
||||
])
|
||||
}
|
||||
@@ -8,6 +8,7 @@ type SetupGuideProgressRingProps = {
|
||||
className?: string
|
||||
sizeClassName?: string
|
||||
strokeWidth?: number
|
||||
tooltipLabel?: string
|
||||
}
|
||||
|
||||
export function SetupGuideProgressRing({
|
||||
@@ -15,7 +16,8 @@ export function SetupGuideProgressRing({
|
||||
total,
|
||||
className,
|
||||
sizeClassName = 'size-5',
|
||||
strokeWidth = 2
|
||||
strokeWidth = 2,
|
||||
tooltipLabel
|
||||
}: SetupGuideProgressRingProps): JSX.Element {
|
||||
const boundedTotal = Math.max(total, 1)
|
||||
const boundedDone = Math.min(Math.max(done, 0), boundedTotal)
|
||||
@@ -62,7 +64,7 @@ export function SetupGuideProgressRing({
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={4}>
|
||||
{progressLabel}
|
||||
{tooltipLabel ?? progressLabel}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
@@ -6,13 +6,13 @@ import {
|
||||
ExternalLink,
|
||||
FolderPlus,
|
||||
HardDrive,
|
||||
ListChecks,
|
||||
MessageSquareText,
|
||||
RotateCw,
|
||||
School,
|
||||
Settings,
|
||||
Smartphone
|
||||
} from 'lucide-react'
|
||||
import logo from '../../../../../resources/logo.svg'
|
||||
import { useAppStore } from '@/store'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
||||
@@ -167,7 +167,12 @@ const SidebarToolbar = React.memo(function SidebarToolbar() {
|
||||
<DropdownMenuItem
|
||||
onSelect={() => openModal('setup-guide', { telemetrySource: 'help_menu' })}
|
||||
>
|
||||
<ListChecks className="size-3.5" />
|
||||
<img
|
||||
src={logo}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="size-3.5 object-contain invert opacity-55 dark:invert-0"
|
||||
/>
|
||||
Onboarding checklist
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
|
||||
@@ -24,12 +24,13 @@ function ids(args: { isMac?: boolean; isWindows?: boolean; isWebClient?: boolean
|
||||
|
||||
describe('settings navigation metadata', () => {
|
||||
it('puts AI capability panes at the top on desktop', () => {
|
||||
expect(ids().slice(0, 8)).toEqual([
|
||||
expect(ids().slice(0, 9)).toEqual([
|
||||
'agents',
|
||||
'accounts',
|
||||
'orchestration',
|
||||
'computer-use',
|
||||
'voice',
|
||||
'setup-guide',
|
||||
'general',
|
||||
'integrations',
|
||||
'git'
|
||||
@@ -37,10 +38,11 @@ describe('settings navigation metadata', () => {
|
||||
})
|
||||
|
||||
it('puts web-safe AI capability panes at the top while hiding desktop-only panes', () => {
|
||||
expect(ids({ isWebClient: true }).slice(0, 6)).toEqual([
|
||||
expect(ids({ isWebClient: true }).slice(0, 7)).toEqual([
|
||||
'agents',
|
||||
'accounts',
|
||||
'orchestration',
|
||||
'setup-guide',
|
||||
'general',
|
||||
'integrations',
|
||||
'git'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* oxlint-disable max-lines */
|
||||
import { useMemo } from 'react'
|
||||
import { createElement, useMemo } from 'react'
|
||||
// Why: this registry mirrors the Settings sidebar in one neutral module so
|
||||
// Cmd+J and Settings visibility cannot drift. Keep it free of Settings pane UI
|
||||
// imports; the boundary is enforced by a focused architecture test.
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
UserCog,
|
||||
Wrench
|
||||
} from 'lucide-react'
|
||||
import type { LucideProps } from 'lucide-react'
|
||||
import logo from '../../../../resources/logo.svg'
|
||||
import type { Repo } from '../../../shared/types'
|
||||
import { getRepoKindLabel } from '../../../shared/repo-kind'
|
||||
import { useAppStore } from '@/store'
|
||||
@@ -65,6 +67,16 @@ import { SHORTCUTS_PANE_SEARCH_ENTRIES } from '@/components/settings/shortcuts-s
|
||||
import { STATS_PANE_SEARCH_ENTRIES } from '@/components/stats/stats-search'
|
||||
import { EXPERIMENTAL_PANE_SEARCH_ENTRIES } from '@/components/settings/experimental-search'
|
||||
import { getRepositoryPaneSearchEntries } from '@/components/settings/repository-search'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function OrcaLogoSettingsIcon({ className }: LucideProps) {
|
||||
return createElement('img', {
|
||||
src: logo,
|
||||
alt: '',
|
||||
'aria-hidden': true,
|
||||
className: cn('object-contain invert dark:invert-0', className)
|
||||
})
|
||||
}
|
||||
|
||||
export function isWebClientLocation(): boolean {
|
||||
return (
|
||||
@@ -142,6 +154,20 @@ export function buildSettingsNavigationMetadata({
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'setup-guide',
|
||||
title: 'Onboarding checklist',
|
||||
description: 'Finish the onboarding checklist for core Orca workflows.',
|
||||
icon: OrcaLogoSettingsIcon,
|
||||
searchEntries: [
|
||||
{
|
||||
title: 'Onboarding checklist',
|
||||
description: 'Open the onboarding checklist for setup and milestone steps.',
|
||||
keywords: ['setup guide', 'get started with Orca', 'getting started']
|
||||
}
|
||||
],
|
||||
group: 'setup'
|
||||
},
|
||||
{
|
||||
id: 'general',
|
||||
title: 'General',
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import type { ComponentType } from 'react'
|
||||
import type { LucideProps } from 'lucide-react'
|
||||
import type { SettingsSearchEntry } from '@/components/settings/settings-search'
|
||||
|
||||
export type SettingsNavIcon = ComponentType<LucideProps>
|
||||
export type SettingsNavInstallStatus = 'install' | 'installed' | 'checking'
|
||||
|
||||
export type SettingsNavTarget =
|
||||
@@ -35,7 +37,7 @@ export type SettingsNavSection = {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
icon: LucideIcon
|
||||
icon: SettingsNavIcon
|
||||
searchEntries: SettingsSearchEntry[]
|
||||
group: string
|
||||
badge?: string
|
||||
|
||||
Reference in New Issue
Block a user