fix: address review findings (#2051)
This commit is contained in:
parent
c3c5a7c6dd
commit
17f4abfc5e
|
|
@ -0,0 +1,69 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
deriveNeededRepoIds,
|
||||
deriveNeededSectionIds,
|
||||
getRuntimeTargetIdentity
|
||||
} from './settings-load-performance'
|
||||
|
||||
describe('Settings load-performance helpers', () => {
|
||||
it('keeps only eager and active sections mounted for empty search on first paint', () => {
|
||||
const needed = deriveNeededSectionIds({
|
||||
navSectionIds: ['general', 'agents', 'appearance', 'terminal', 'stats', 'ssh', 'repo-a'],
|
||||
mountedSectionIds: new Set(['general']),
|
||||
activeSectionId: 'general',
|
||||
pendingSectionId: null,
|
||||
query: '',
|
||||
visibleSectionIds: new Set([
|
||||
'general',
|
||||
'agents',
|
||||
'appearance',
|
||||
'terminal',
|
||||
'stats',
|
||||
'ssh',
|
||||
'repo-a'
|
||||
])
|
||||
})
|
||||
|
||||
expect(Array.from(needed).sort()).toEqual(['general'])
|
||||
})
|
||||
|
||||
it('adds matched sections immediately when search is non-empty', () => {
|
||||
const needed = deriveNeededSectionIds({
|
||||
navSectionIds: ['general', 'agents', 'appearance', 'terminal', 'stats', 'repo-a'],
|
||||
mountedSectionIds: new Set(['general']),
|
||||
activeSectionId: 'general',
|
||||
pendingSectionId: null,
|
||||
query: 'stats',
|
||||
visibleSectionIds: new Set(['stats'])
|
||||
})
|
||||
|
||||
expect(needed.has('stats')).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps a pending deep-link target mounted before jump work continues', () => {
|
||||
const needed = deriveNeededSectionIds({
|
||||
navSectionIds: ['general', 'agents', 'appearance', 'terminal', 'repo-a'],
|
||||
mountedSectionIds: new Set(['general']),
|
||||
activeSectionId: 'general',
|
||||
pendingSectionId: 'repo-a',
|
||||
query: '',
|
||||
visibleSectionIds: new Set(['general', 'agents', 'appearance', 'terminal', 'repo-a'])
|
||||
})
|
||||
|
||||
expect(needed.has('repo-a')).toBe(true)
|
||||
})
|
||||
|
||||
it('scopes repo hook checks to needed repo sections only', () => {
|
||||
const neededRepoIds = deriveNeededRepoIds(
|
||||
[{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
||||
new Set(['general', 'repo-b'])
|
||||
)
|
||||
|
||||
expect(neededRepoIds).toEqual(['b'])
|
||||
})
|
||||
|
||||
it('normalizes runtime target identity for cache invalidation keys', () => {
|
||||
expect(getRuntimeTargetIdentity({ activeRuntimeEnvironmentId: null })).toBe('local')
|
||||
expect(getRuntimeTargetIdentity({ activeRuntimeEnvironmentId: ' env-1 ' })).toBe('env-1')
|
||||
})
|
||||
})
|
||||
|
|
@ -76,6 +76,12 @@ import { SettingsSidebar } from './SettingsSidebar'
|
|||
import { SettingsSection } from './SettingsSection'
|
||||
import { matchesSettingsSearch, type SettingsSearchEntry } from './settings-search'
|
||||
import { checkRuntimeHooks } from '@/runtime/runtime-hooks-client'
|
||||
import {
|
||||
deriveNeededRepoIds,
|
||||
deriveNeededSectionIds,
|
||||
getInitialMountedSectionIds,
|
||||
getRuntimeTargetIdentity
|
||||
} from './settings-load-performance'
|
||||
|
||||
type SettingsNavTarget =
|
||||
| 'general'
|
||||
|
|
@ -230,20 +236,13 @@ function Settings(): React.JSX.Element {
|
|||
const ghostty = useGhosttyImport(updateSettings, settings)
|
||||
const [wslAvailable, setWslAvailable] = useState(false)
|
||||
const [pwshAvailable, setPwshAvailable] = useState(false)
|
||||
useEffect(() => {
|
||||
if (!isWindows) {
|
||||
setWslAvailable(false)
|
||||
setPwshAvailable(false)
|
||||
return
|
||||
}
|
||||
|
||||
void window.api.wsl.isAvailable().then(setWslAvailable)
|
||||
void window.api.pwsh.isAvailable().then(setPwshAvailable)
|
||||
}, [isWindows])
|
||||
const [fontSuggestions, setFontSuggestions] = useState<string[]>(
|
||||
Array.from(new Set([DEFAULT_APP_FONT_FAMILY, ...getFallbackTerminalFonts()]))
|
||||
)
|
||||
const [activeSectionId, setActiveSectionId] = useState('general')
|
||||
const [mountedSectionIds, setMountedSectionIds] = useState<Set<string>>(
|
||||
getInitialMountedSectionIds
|
||||
)
|
||||
const [pendingNavRequestTick, setPendingNavRequestTick] = useState(0)
|
||||
const [hasUnsavedCommitPromptChanges, setHasUnsavedCommitPromptChanges] = useState(false)
|
||||
const [commitPromptDiscardSignal, setCommitPromptDiscardSignal] = useState(0)
|
||||
|
|
@ -255,8 +254,11 @@ function Settings(): React.JSX.Element {
|
|||
const contentScrollRef = useRef<HTMLDivElement | null>(null)
|
||||
const searchInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const terminalFontsLoadedRef = useRef(false)
|
||||
const terminalCapabilitiesLoadedRef = useRef(false)
|
||||
const pendingNavSectionRef = useRef<string | null>(null)
|
||||
const pendingScrollTargetRef = useRef<string | null>(null)
|
||||
const repoHooksRequestSeqRef = useRef(0)
|
||||
const repoHooksRuntimeIdentityRef = useRef<string>('local')
|
||||
|
||||
const confirmDiscardCommitPromptChanges = useCallback((): boolean => {
|
||||
if (!hasUnsavedCommitPromptChanges) {
|
||||
|
|
@ -283,6 +285,8 @@ function Settings(): React.JSX.Element {
|
|||
fetchSettings()
|
||||
}, [fetchSettings])
|
||||
|
||||
const runtimeTargetIdentity = getRuntimeTargetIdentity(settings)
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||
if (event.key !== 'Escape' || event.defaultPrevented) {
|
||||
|
|
@ -361,35 +365,6 @@ function Settings(): React.JSX.Element {
|
|||
clearSettingsTarget()
|
||||
}, [clearSettingsTarget, settings, settingsNavigationTarget])
|
||||
|
||||
useEffect(() => {
|
||||
if (terminalFontsLoadedRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
let stale = false
|
||||
|
||||
const loadFontSuggestions = async (): Promise<void> => {
|
||||
try {
|
||||
const fonts = await window.api.settings.listFonts()
|
||||
if (stale || fonts.length === 0) {
|
||||
return
|
||||
}
|
||||
terminalFontsLoadedRef.current = true
|
||||
setFontSuggestions((prev) =>
|
||||
Array.from(new Set([DEFAULT_APP_FONT_FAMILY, ...fonts, ...prev])).slice(0, 320)
|
||||
)
|
||||
} catch {
|
||||
// Fall back to curated cross-platform suggestions.
|
||||
}
|
||||
}
|
||||
|
||||
void loadFontSuggestions()
|
||||
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Why: only recompute scrollback mode when the byte value actually changes,
|
||||
// not on every unrelated settings mutation.
|
||||
if (settings?.terminalScrollbackBytes !== prevScrollbackBytes) {
|
||||
|
|
@ -404,45 +379,6 @@ function Settings(): React.JSX.Element {
|
|||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let stale = false
|
||||
|
||||
const checkHooks = async (): Promise<void> => {
|
||||
const results = await Promise.all(
|
||||
repos.map(async (repo) => {
|
||||
if (isFolderRepo(repo)) {
|
||||
return [repo.id, { hasHooks: false, hooks: null, mayNeedUpdate: false }] as const
|
||||
}
|
||||
try {
|
||||
const result = await checkRuntimeHooks(settings, repo.id)
|
||||
return [repo.id, result] as const
|
||||
} catch {
|
||||
return [repo.id, { hasHooks: false, hooks: null, mayNeedUpdate: false }] as const
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
if (!stale) {
|
||||
setRepoHooksMap(
|
||||
Object.fromEntries(results) as Record<
|
||||
string,
|
||||
{ hasHooks: boolean; hooks: OrcaHooks | null; mayNeedUpdate: boolean }
|
||||
>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (repos.length > 0) {
|
||||
void checkHooks()
|
||||
} else {
|
||||
setRepoHooksMap({})
|
||||
}
|
||||
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}, [repos, settings])
|
||||
|
||||
const applyTheme = useCallback((theme: 'system' | 'dark' | 'light') => {
|
||||
applyDocumentTheme(theme)
|
||||
}, [])
|
||||
|
|
@ -653,13 +589,191 @@ function Settings(): React.JSX.Element {
|
|||
),
|
||||
[hasUnsavedCommitPromptChanges, navSections, settingsSearchQuery]
|
||||
)
|
||||
const visibleSectionIds = useMemo(
|
||||
() => new Set(visibleNavSections.map((section) => section.id)),
|
||||
[visibleNavSections]
|
||||
)
|
||||
const neededSectionIds = useMemo(
|
||||
() =>
|
||||
deriveNeededSectionIds({
|
||||
navSectionIds: navSections.map((section) => section.id),
|
||||
mountedSectionIds,
|
||||
activeSectionId,
|
||||
pendingSectionId: pendingNavSectionRef.current,
|
||||
query: settingsSearchQuery,
|
||||
visibleSectionIds
|
||||
}),
|
||||
[activeSectionId, mountedSectionIds, navSections, settingsSearchQuery, visibleSectionIds]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setMountedSectionIds((previous) => {
|
||||
let changed = false
|
||||
const next = new Set(previous)
|
||||
for (const id of neededSectionIds) {
|
||||
if (!next.has(id)) {
|
||||
next.add(id)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return changed ? next : previous
|
||||
})
|
||||
}, [neededSectionIds])
|
||||
|
||||
useEffect(() => {
|
||||
if (!neededSectionIds.has('appearance') && !neededSectionIds.has('terminal')) {
|
||||
return
|
||||
}
|
||||
if (terminalFontsLoadedRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
let stale = false
|
||||
const loadFontSuggestions = async (): Promise<void> => {
|
||||
try {
|
||||
const fonts = await window.api.settings.listFonts()
|
||||
if (stale || fonts.length === 0) {
|
||||
return
|
||||
}
|
||||
terminalFontsLoadedRef.current = true
|
||||
setFontSuggestions((prev) =>
|
||||
Array.from(new Set([DEFAULT_APP_FONT_FAMILY, ...fonts, ...prev])).slice(0, 320)
|
||||
)
|
||||
} catch {
|
||||
// Fall back to curated cross-platform suggestions.
|
||||
}
|
||||
}
|
||||
void loadFontSuggestions()
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}, [neededSectionIds])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWindows) {
|
||||
setWslAvailable(false)
|
||||
setPwshAvailable(false)
|
||||
terminalCapabilitiesLoadedRef.current = true
|
||||
return
|
||||
}
|
||||
if (!neededSectionIds.has('terminal') || terminalCapabilitiesLoadedRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
let stale = false
|
||||
terminalCapabilitiesLoadedRef.current = true
|
||||
void window.api.wsl.isAvailable().then((available) => {
|
||||
if (!stale) {
|
||||
setWslAvailable(available)
|
||||
}
|
||||
})
|
||||
void window.api.pwsh.isAvailable().then((available) => {
|
||||
if (!stale) {
|
||||
setPwshAvailable(available)
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}, [isWindows, neededSectionIds])
|
||||
|
||||
const neededRepoIds = useMemo(
|
||||
() => deriveNeededRepoIds(repos, neededSectionIds),
|
||||
[neededSectionIds, repos]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const repoIdSet = new Set(repos.map((repo) => repo.id))
|
||||
setRepoHooksMap((previous) => {
|
||||
const next = Object.fromEntries(
|
||||
Object.entries(previous).filter(([repoId]) => repoIdSet.has(repoId))
|
||||
) as Record<string, { hasHooks: boolean; hooks: OrcaHooks | null; mayNeedUpdate: boolean }>
|
||||
return Object.keys(next).length === Object.keys(previous).length ? previous : next
|
||||
})
|
||||
}, [repos])
|
||||
|
||||
useEffect(() => {
|
||||
if (repoHooksRuntimeIdentityRef.current !== runtimeTargetIdentity) {
|
||||
repoHooksRuntimeIdentityRef.current = runtimeTargetIdentity
|
||||
repoHooksRequestSeqRef.current += 1
|
||||
setRepoHooksMap({})
|
||||
}
|
||||
}, [runtimeTargetIdentity])
|
||||
|
||||
useEffect(() => {
|
||||
if (neededRepoIds.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let stale = false
|
||||
const requestSeq = ++repoHooksRequestSeqRef.current
|
||||
const repoById = new Map(repos.map((repo) => [repo.id, repo] as const))
|
||||
|
||||
void Promise.all(
|
||||
neededRepoIds.map(async (repoId) => {
|
||||
const repo = repoById.get(repoId)
|
||||
if (!repo) {
|
||||
return
|
||||
}
|
||||
if (isFolderRepo(repo)) {
|
||||
setRepoHooksMap((previous) => {
|
||||
if (previous[repoId]) {
|
||||
return previous
|
||||
}
|
||||
return {
|
||||
...previous,
|
||||
[repoId]: { hasHooks: false, hooks: null, mayNeedUpdate: false }
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
try {
|
||||
const result = await checkRuntimeHooks(
|
||||
runtimeTargetIdentity === 'local'
|
||||
? { activeRuntimeEnvironmentId: null }
|
||||
: { activeRuntimeEnvironmentId: runtimeTargetIdentity },
|
||||
repoId
|
||||
)
|
||||
if (stale || requestSeq !== repoHooksRequestSeqRef.current) {
|
||||
return
|
||||
}
|
||||
setRepoHooksMap((previous) => {
|
||||
if (!repos.some((entry) => entry.id === repoId)) {
|
||||
return previous
|
||||
}
|
||||
return { ...previous, [repoId]: result }
|
||||
})
|
||||
} catch {
|
||||
// Keep last known value on transient failures.
|
||||
if (stale || requestSeq !== repoHooksRequestSeqRef.current) {
|
||||
return
|
||||
}
|
||||
setRepoHooksMap((previous) => {
|
||||
if (!repos.some((entry) => entry.id === repoId)) {
|
||||
return previous
|
||||
}
|
||||
if (previous[repoId]) {
|
||||
return previous
|
||||
}
|
||||
return {
|
||||
...previous,
|
||||
[repoId]: { hasHooks: false, hooks: null, mayNeedUpdate: false }
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}, [neededRepoIds, repos, runtimeTargetIdentity])
|
||||
|
||||
useEffect(() => {
|
||||
const scrollTargetId = pendingScrollTargetRef.current
|
||||
const pendingNavSectionId = pendingNavSectionRef.current
|
||||
const visibleIds = new Set(visibleNavSections.map((section) => section.id))
|
||||
|
||||
if (scrollTargetId && pendingNavSectionId && visibleIds.has(pendingNavSectionId)) {
|
||||
if (scrollTargetId && pendingNavSectionId && visibleSectionIds.has(pendingNavSectionId)) {
|
||||
scrollSectionIntoView(scrollTargetId, contentScrollRef.current)
|
||||
flashSectionHighlight(scrollTargetId)
|
||||
setActiveSectionId(pendingNavSectionId)
|
||||
|
|
@ -673,7 +787,7 @@ function Settings(): React.JSX.Element {
|
|||
return
|
||||
}
|
||||
|
||||
if (!visibleIds.has(activeSectionId) && visibleNavSections.length > 0) {
|
||||
if (!visibleSectionIds.has(activeSectionId) && visibleNavSections.length > 0) {
|
||||
setActiveSectionId(getFallbackVisibleSection(visibleNavSections)?.id ?? activeSectionId)
|
||||
}
|
||||
}, [
|
||||
|
|
@ -681,6 +795,7 @@ function Settings(): React.JSX.Element {
|
|||
pendingNavRequestTick,
|
||||
setSettingsSearchQuery,
|
||||
settingsSearchQuery,
|
||||
visibleSectionIds,
|
||||
visibleNavSections
|
||||
])
|
||||
|
||||
|
|
@ -807,6 +922,7 @@ function Settings(): React.JSX.Element {
|
|||
const repo = repos.find((entry) => entry.id === section.id.replace('repo-', ''))
|
||||
return { ...section, badgeColor: repo?.badgeColor, isRemote: !!repo?.connectionId }
|
||||
})
|
||||
const isSectionMounted = (sectionId: string): boolean => neededSectionIds.has(sectionId)
|
||||
|
||||
return (
|
||||
<div className="settings-view-shell flex min-h-0 flex-1 overflow-hidden bg-background">
|
||||
|
|
@ -837,7 +953,9 @@ function Settings(): React.JSX.Element {
|
|||
description="Workspace, editor, and updates."
|
||||
searchEntries={GENERAL_PANE_SEARCH_ENTRIES}
|
||||
>
|
||||
<GeneralPane settings={settings} updateSettings={updateSettings} />
|
||||
{isSectionMounted('general') ? (
|
||||
<GeneralPane settings={settings} updateSettings={updateSettings} />
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
|
|
@ -846,7 +964,7 @@ function Settings(): React.JSX.Element {
|
|||
description="GitHub, Linear, and other service connections."
|
||||
searchEntries={INTEGRATIONS_PANE_SEARCH_ENTRIES}
|
||||
>
|
||||
<IntegrationsPane />
|
||||
{isSectionMounted('integrations') ? <IntegrationsPane /> : null}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
|
|
@ -855,7 +973,9 @@ function Settings(): React.JSX.Element {
|
|||
description="Manage AI agents, set a default, and customize commands."
|
||||
searchEntries={AGENTS_PANE_SEARCH_ENTRIES}
|
||||
>
|
||||
<AgentsPane settings={settings} updateSettings={updateSettings} />
|
||||
{isSectionMounted('agents') ? (
|
||||
<AgentsPane settings={settings} updateSettings={updateSettings} />
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
|
|
@ -864,7 +984,9 @@ function Settings(): React.JSX.Element {
|
|||
description="Sign in and switch between Claude, Codex, Gemini, and OpenCode Go accounts."
|
||||
searchEntries={ACCOUNTS_PANE_SEARCH_ENTRIES}
|
||||
>
|
||||
<AccountsPane settings={settings} updateSettings={updateSettings} />
|
||||
{isSectionMounted('accounts') ? (
|
||||
<AccountsPane settings={settings} updateSettings={updateSettings} />
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
|
|
@ -877,17 +999,21 @@ function Settings(): React.JSX.Element {
|
|||
]}
|
||||
forceVisible={hasUnsavedCommitPromptChanges}
|
||||
>
|
||||
<GitPane
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
displayedGitUsername={displayedGitUsername}
|
||||
/>
|
||||
<CommitMessageAiPane
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
onCustomPromptDirtyChange={setHasUnsavedCommitPromptChanges}
|
||||
customPromptDiscardSignal={commitPromptDiscardSignal}
|
||||
/>
|
||||
{isSectionMounted('git') ? (
|
||||
<>
|
||||
<GitPane
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
displayedGitUsername={displayedGitUsername}
|
||||
/>
|
||||
<CommitMessageAiPane
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
onCustomPromptDirtyChange={setHasUnsavedCommitPromptChanges}
|
||||
customPromptDiscardSignal={commitPromptDiscardSignal}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
|
|
@ -896,7 +1022,9 @@ function Settings(): React.JSX.Element {
|
|||
description="Choose which task providers appear in the Tasks page and sidebar."
|
||||
searchEntries={TASKS_PANE_SEARCH_ENTRIES}
|
||||
>
|
||||
<TasksPane settings={settings} updateSettings={updateSettings} />
|
||||
{isSectionMounted('tasks') ? (
|
||||
<TasksPane settings={settings} updateSettings={updateSettings} />
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
|
|
@ -905,12 +1033,14 @@ function Settings(): React.JSX.Element {
|
|||
description="Theme and UI scaling."
|
||||
searchEntries={APPEARANCE_PANE_SEARCH_ENTRIES}
|
||||
>
|
||||
<AppearancePane
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
applyTheme={applyTheme}
|
||||
fontSuggestions={fontSuggestions}
|
||||
/>
|
||||
{isSectionMounted('appearance') ? (
|
||||
<AppearancePane
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
applyTheme={applyTheme}
|
||||
fontSuggestions={fontSuggestions}
|
||||
/>
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
|
|
@ -930,19 +1060,21 @@ function Settings(): React.JSX.Element {
|
|||
</Button>
|
||||
}
|
||||
>
|
||||
<TerminalPane
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
systemPrefersDark={systemPrefersDark}
|
||||
terminalFontSuggestions={fontSuggestions.filter(
|
||||
(font) => font !== DEFAULT_APP_FONT_FAMILY
|
||||
)}
|
||||
scrollbackMode={scrollbackMode}
|
||||
setScrollbackMode={setScrollbackMode}
|
||||
ghostty={ghostty}
|
||||
wslAvailable={wslAvailable}
|
||||
pwshAvailable={pwshAvailable}
|
||||
/>
|
||||
{isSectionMounted('terminal') ? (
|
||||
<TerminalPane
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
systemPrefersDark={systemPrefersDark}
|
||||
terminalFontSuggestions={fontSuggestions.filter(
|
||||
(font) => font !== DEFAULT_APP_FONT_FAMILY
|
||||
)}
|
||||
scrollbackMode={scrollbackMode}
|
||||
setScrollbackMode={setScrollbackMode}
|
||||
ghostty={ghostty}
|
||||
wslAvailable={wslAvailable}
|
||||
pwshAvailable={pwshAvailable}
|
||||
/>
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
|
||||
{showDesktopOnlySettings ? (
|
||||
|
|
@ -953,11 +1085,13 @@ function Settings(): React.JSX.Element {
|
|||
description="Home page, link routing, and session cookies."
|
||||
searchEntries={BROWSER_PANE_SEARCH_ENTRIES}
|
||||
>
|
||||
<BrowserPane
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
onOpenComputerUse={openComputerUseFromBrowser}
|
||||
/>
|
||||
{isSectionMounted('browser') ? (
|
||||
<BrowserPane
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
onOpenComputerUse={openComputerUseFromBrowser}
|
||||
/>
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
|
|
@ -966,7 +1100,9 @@ function Settings(): React.JSX.Element {
|
|||
description="Native desktop notifications for agent activity and terminal events."
|
||||
searchEntries={NOTIFICATIONS_PANE_SEARCH_ENTRIES}
|
||||
>
|
||||
<NotificationsPane settings={settings} updateSettings={updateSettings} />
|
||||
{isSectionMounted('notifications') ? (
|
||||
<NotificationsPane settings={settings} updateSettings={updateSettings} />
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
</>
|
||||
) : null}
|
||||
|
|
@ -977,7 +1113,7 @@ function Settings(): React.JSX.Element {
|
|||
description="Coordinate multiple coding agents through Orca."
|
||||
searchEntries={ORCHESTRATION_PANE_SEARCH_ENTRIES}
|
||||
>
|
||||
<OrchestrationPane />
|
||||
{isSectionMounted('orchestration') ? <OrchestrationPane /> : null}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
|
|
@ -991,12 +1127,14 @@ function Settings(): React.JSX.Element {
|
|||
}
|
||||
searchEntries={[runtimeEnvironmentsSearchEntry]}
|
||||
>
|
||||
<RuntimeEnvironmentsPane
|
||||
settings={settings}
|
||||
switchRuntimeEnvironment={switchRuntimeEnvironment}
|
||||
canGeneratePairingUrl={!isWebClient}
|
||||
allowLocalRuntime={!isWebClient}
|
||||
/>
|
||||
{isSectionMounted('servers') ? (
|
||||
<RuntimeEnvironmentsPane
|
||||
settings={settings}
|
||||
switchRuntimeEnvironment={switchRuntimeEnvironment}
|
||||
canGeneratePairingUrl={!isWebClient}
|
||||
allowLocalRuntime={!isWebClient}
|
||||
/>
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
|
||||
{showDesktopOnlySettings ? (
|
||||
|
|
@ -1008,7 +1146,9 @@ function Settings(): React.JSX.Element {
|
|||
description="Control terminals and agents from your phone."
|
||||
searchEntries={MOBILE_SETTINGS_PANE_SEARCH_ENTRIES}
|
||||
>
|
||||
<MobileSettingsPane settings={settings} updateSettings={updateSettings} />
|
||||
{isSectionMounted('mobile') ? (
|
||||
<MobileSettingsPane settings={settings} updateSettings={updateSettings} />
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
|
|
@ -1041,7 +1181,7 @@ function Settings(): React.JSX.Element {
|
|||
description="Enable agents to control any app on your computer."
|
||||
searchEntries={COMPUTER_USE_PANE_SEARCH_ENTRIES}
|
||||
>
|
||||
<ComputerUsePane />
|
||||
{isSectionMounted('computer-use') ? <ComputerUsePane /> : null}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
|
|
@ -1051,7 +1191,9 @@ function Settings(): React.JSX.Element {
|
|||
description="Local speech-to-text dictation with on-device models."
|
||||
searchEntries={VOICE_PANE_SEARCH_ENTRIES}
|
||||
>
|
||||
<VoicePane settings={settings} updateSettings={updateSettings} />
|
||||
{isSectionMounted('voice') ? (
|
||||
<VoicePane settings={settings} updateSettings={updateSettings} />
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
</>
|
||||
) : null}
|
||||
|
|
@ -1063,7 +1205,9 @@ function Settings(): React.JSX.Element {
|
|||
description="macOS privacy access for terminal-launched developer tools."
|
||||
searchEntries={DEVELOPER_PERMISSIONS_PANE_SEARCH_ENTRIES}
|
||||
>
|
||||
<DeveloperPermissionsPane />
|
||||
{isSectionMounted('developer-permissions') ? (
|
||||
<DeveloperPermissionsPane />
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
) : null}
|
||||
|
||||
|
|
@ -1073,7 +1217,7 @@ function Settings(): React.JSX.Element {
|
|||
description="Anonymous usage data and telemetry controls."
|
||||
searchEntries={PRIVACY_PANE_SEARCH_ENTRIES}
|
||||
>
|
||||
<PrivacyPane settings={settings} />
|
||||
{isSectionMounted('privacy') ? <PrivacyPane settings={settings} /> : null}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
|
|
@ -1082,7 +1226,7 @@ function Settings(): React.JSX.Element {
|
|||
description="Keyboard shortcuts for common actions."
|
||||
searchEntries={SHORTCUTS_PANE_SEARCH_ENTRIES}
|
||||
>
|
||||
<ShortcutsPane />
|
||||
{isSectionMounted('shortcuts') ? <ShortcutsPane /> : null}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
|
|
@ -1091,7 +1235,7 @@ function Settings(): React.JSX.Element {
|
|||
description="How much Orca has helped you."
|
||||
searchEntries={STATS_PANE_SEARCH_ENTRIES}
|
||||
>
|
||||
<StatsPane />
|
||||
{isSectionMounted('stats') ? <StatsPane /> : null}
|
||||
</SettingsSection>
|
||||
|
||||
{showDesktopOnlySettings ? (
|
||||
|
|
@ -1101,7 +1245,7 @@ function Settings(): React.JSX.Element {
|
|||
description="Manage remote SSH connections. Connect to remote servers to browse files, run terminals, and use git."
|
||||
searchEntries={SSH_PANE_SEARCH_ENTRIES}
|
||||
>
|
||||
<SshPane />
|
||||
{isSectionMounted('ssh') ? <SshPane /> : null}
|
||||
</SettingsSection>
|
||||
) : null}
|
||||
|
||||
|
|
@ -1111,11 +1255,13 @@ function Settings(): React.JSX.Element {
|
|||
description="New features that are still taking shape. Give them a try."
|
||||
searchEntries={EXPERIMENTAL_PANE_SEARCH_ENTRIES}
|
||||
>
|
||||
<ExperimentalPane
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
hiddenExperimentalUnlocked={hiddenExperimentalUnlocked}
|
||||
/>
|
||||
{isSectionMounted('experimental') ? (
|
||||
<ExperimentalPane
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
hiddenExperimentalUnlocked={hiddenExperimentalUnlocked}
|
||||
/>
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
|
||||
{repos.map((repo) => {
|
||||
|
|
@ -1130,14 +1276,16 @@ function Settings(): React.JSX.Element {
|
|||
description={repo.path}
|
||||
searchEntries={getRepositoryPaneSearchEntries(repo)}
|
||||
>
|
||||
<RepositoryPane
|
||||
repo={repo}
|
||||
yamlHooks={repoHooksState?.hooks ?? null}
|
||||
hasHooksFile={repoHooksState?.hasHooks ?? false}
|
||||
mayNeedUpdate={repoHooksState?.mayNeedUpdate ?? false}
|
||||
updateRepo={updateRepo}
|
||||
removeRepo={removeRepo}
|
||||
/>
|
||||
{isSectionMounted(repoSectionId) ? (
|
||||
<RepositoryPane
|
||||
repo={repo}
|
||||
yamlHooks={repoHooksState?.hooks ?? null}
|
||||
hasHooksFile={repoHooksState?.hasHooks ?? false}
|
||||
mayNeedUpdate={repoHooksState?.mayNeedUpdate ?? false}
|
||||
updateRepo={updateRepo}
|
||||
removeRepo={removeRepo}
|
||||
/>
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
)
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import type React from 'react'
|
||||
import { useAppStore } from '../../store'
|
||||
import { matchesSettingsSearch, type SettingsSearchEntry } from './settings-search'
|
||||
import type { SettingsSearchEntry } from './settings-search'
|
||||
import { matchesSettingsSearch } from './settings-search'
|
||||
|
||||
type SettingsSectionProps = {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
searchEntries: SettingsSearchEntry[]
|
||||
children: React.ReactNode
|
||||
searchEntries?: SettingsSearchEntry[]
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
badge?: string
|
||||
badgeAccessory?: React.ReactNode
|
||||
|
|
@ -31,7 +32,7 @@ export function SettingsSection({
|
|||
headerAction
|
||||
}: SettingsSectionProps): React.JSX.Element | null {
|
||||
const query = useAppStore((state) => state.settingsSearchQuery)
|
||||
if (!forceVisible && !matchesSettingsSearch(query, searchEntries)) {
|
||||
if (!forceVisible && searchEntries && !matchesSettingsSearch(query, searchEntries)) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
import type { GlobalSettings } from '../../../../shared/types'
|
||||
|
||||
const EAGER_SECTION_IDS = new Set(['general'])
|
||||
|
||||
export function getRuntimeTargetIdentity(
|
||||
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined
|
||||
): string {
|
||||
return settings?.activeRuntimeEnvironmentId?.trim() || 'local'
|
||||
}
|
||||
|
||||
export function deriveNeededSectionIds(args: {
|
||||
navSectionIds: string[]
|
||||
mountedSectionIds: Set<string>
|
||||
activeSectionId: string | null
|
||||
pendingSectionId: string | null
|
||||
query: string
|
||||
visibleSectionIds: Set<string>
|
||||
}): Set<string> {
|
||||
const next = new Set(args.mountedSectionIds)
|
||||
for (const sectionId of args.navSectionIds) {
|
||||
if (EAGER_SECTION_IDS.has(sectionId)) {
|
||||
next.add(sectionId)
|
||||
}
|
||||
}
|
||||
if (args.activeSectionId) {
|
||||
next.add(args.activeSectionId)
|
||||
}
|
||||
if (args.pendingSectionId) {
|
||||
next.add(args.pendingSectionId)
|
||||
}
|
||||
if (args.query.trim() !== '') {
|
||||
for (const visibleSectionId of args.visibleSectionIds) {
|
||||
next.add(visibleSectionId)
|
||||
}
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
export function deriveNeededRepoIds(
|
||||
repos: readonly { id: string }[],
|
||||
neededSectionIds: Set<string>
|
||||
): string[] {
|
||||
return repos.map((repo) => repo.id).filter((repoId) => neededSectionIds.has(`repo-${repoId}`))
|
||||
}
|
||||
|
||||
export function getInitialMountedSectionIds(): Set<string> {
|
||||
return new Set(EAGER_SECTION_IDS)
|
||||
}
|
||||
Loading…
Reference in New Issue