Add HTTP/1.1 compatibility mode (#4509)

This commit is contained in:
Neil 2026-06-02 16:54:11 -07:00 committed by GitHub
parent 0d342235a7
commit d879cfde37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 483 additions and 59 deletions

View File

@ -44,6 +44,7 @@ import {
} from './menu/register-app-menu'
import { checkForUpdatesFromMenu, isQuittingForUpdate } from './updater'
import {
configureElectronNetworkCompatibility,
configureDevUserDataPath,
configureOrcaUserDataPathEnv,
enableMainProcessGpuFeatures,
@ -370,6 +371,7 @@ if (hasSingleInstanceLock) {
packaged: app.isPackaged,
platform: process.platform
})
configureElectronNetworkCompatibility()
enableMainProcessGpuFeatures()
}
@ -543,6 +545,7 @@ function openMainWindow(): BrowserWindow {
{
onBeforeRelaunch: () => {
isQuitting = true
store?.flush()
}
}
)

View File

@ -1,4 +1,5 @@
import { homedir } from 'os'
import { mkdtempSync, rmSync, writeFileSync } from 'fs'
import { homedir, tmpdir } from 'os'
import { join } from 'path'
import { afterEach, describe, expect, it, vi } from 'vitest'
@ -189,6 +190,76 @@ describe('shouldInstallManagedHooks', () => {
})
})
describe('configureElectronNetworkCompatibility', () => {
const tempDirs: string[] = []
const originalEnvValue = process.env.ORCA_DISABLE_HTTP2
function createUserDataDir(settings: Record<string, unknown>): string {
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-http1-compat-'))
tempDirs.push(userDataPath)
writeFileSync(join(userDataPath, 'orca-data.json'), JSON.stringify({ settings }), 'utf-8')
return userDataPath
}
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true })
}
if (originalEnvValue === undefined) {
delete process.env.ORCA_DISABLE_HTTP2
} else {
process.env.ORCA_DISABLE_HTTP2 = originalEnvValue
}
})
it('enables HTTP/1.1 compatibility when the persisted setting is on', async () => {
const { shouldDisableHttp2ForElectronNetworking } = await import('./configure-process')
const userDataPath = createUserDataDir({ electronHttp1CompatibilityMode: true })
expect(shouldDisableHttp2ForElectronNetworking({ env: {}, userDataPath })).toBe(true)
})
it('leaves HTTP/2 enabled by default', async () => {
const { shouldDisableHttp2ForElectronNetworking } = await import('./configure-process')
const userDataPath = createUserDataDir({})
expect(shouldDisableHttp2ForElectronNetworking({ env: {}, userDataPath })).toBe(false)
})
it('lets the environment override force compatibility on', async () => {
const { shouldDisableHttp2ForElectronNetworking } = await import('./configure-process')
expect(
shouldDisableHttp2ForElectronNetworking({
env: { ORCA_DISABLE_HTTP2: 'true' },
userDataPath: createUserDataDir({ electronHttp1CompatibilityMode: false })
})
).toBe(true)
})
it('lets the environment override force compatibility off', async () => {
const { shouldDisableHttp2ForElectronNetworking } = await import('./configure-process')
expect(
shouldDisableHttp2ForElectronNetworking({
env: { ORCA_DISABLE_HTTP2: '0' },
userDataPath: createUserDataDir({ electronHttp1CompatibilityMode: true })
})
).toBe(false)
})
it('appends Electron disable-http2 before sessions are created', async () => {
const { app } = await import('electron')
const { configureElectronNetworkCompatibility } = await import('./configure-process')
const userDataPath = createUserDataDir({ electronHttp1CompatibilityMode: true })
vi.mocked(app.commandLine.appendSwitch).mockClear()
configureElectronNetworkCompatibility({ env: {}, userDataPath })
expect(app.commandLine.appendSwitch).toHaveBeenCalledWith('disable-http2')
})
})
describe('enableMainProcessGpuFeatures', () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
const originalE2EUserDataDir = process.env.ORCA_E2E_USER_DATA_DIR

View File

@ -1,11 +1,71 @@
import { app } from 'electron'
import { existsSync, readFileSync } from 'fs'
import { join } from 'path'
import { getVersionManagerBinPaths } from '../codex-cli/command'
import { getMainE2EConfig } from '../e2e-config'
const DEV_PARENT_SHUTDOWN_GRACE_MS = 3000
const HTTP1_COMPATIBILITY_ENV_VAR = 'ORCA_DISABLE_HTTP2'
const TRUE_ENV_VALUES = new Set(['1', 'true', 'yes', 'on'])
const FALSE_ENV_VALUES = new Set(['0', 'false', 'no', 'off'])
let devParentShutdownRequested = false
type NetworkCompatibilityOptions = {
env?: NodeJS.ProcessEnv
userDataPath?: string
}
function parseBooleanEnvFlag(value: string | undefined): boolean | null {
if (value === undefined) {
return null
}
const normalized = value.trim().toLowerCase()
if (TRUE_ENV_VALUES.has(normalized)) {
return true
}
if (FALSE_ENV_VALUES.has(normalized)) {
return false
}
return null
}
function readPersistedHttp1CompatibilityMode(userDataPath: string): boolean {
const dataFile = join(userDataPath, 'orca-data.json')
if (!existsSync(dataFile)) {
return false
}
try {
const parsed = JSON.parse(readFileSync(dataFile, 'utf-8')) as {
settings?: { electronHttp1CompatibilityMode?: unknown }
}
return parsed.settings?.electronHttp1CompatibilityMode === true
} catch {
return false
}
}
export function shouldDisableHttp2ForElectronNetworking(
options: NetworkCompatibilityOptions = {}
): boolean {
const envValue = parseBooleanEnvFlag(options.env?.[HTTP1_COMPATIBILITY_ENV_VAR])
if (envValue !== null) {
return envValue
}
return readPersistedHttp1CompatibilityMode(options.userDataPath ?? app.getPath('userData'))
}
export function configureElectronNetworkCompatibility(
options: NetworkCompatibilityOptions = {}
): void {
if (!shouldDisableHttp2ForElectronNetworking(options)) {
return
}
// Why: Chromium's HTTP/2 switch is process-wide and only works before the
// first session exists, so read the persisted setting during early startup.
app.commandLine.appendSwitch('disable-http2')
}
function getProcessPathDelimiter(): string {
return process.platform === 'win32' ? ';' : ':'
}

View File

@ -6,6 +6,7 @@ import { getDefaultUIState } from '../../../shared/constants'
import type { ChangelogData, UpdateStatus } from '../../../shared/types'
import { createUISlice } from '../store/slices/ui'
import type { AppState } from '../store/types'
import { isHttp2ProtocolError } from './UpdateCard'
// ── Helpers ──────────────────────────────────────────────────────────
@ -527,6 +528,15 @@ describe('UpdateCard visibility gates', () => {
})
})
describe('HTTP/2 update error detection', () => {
it('recognizes Electron HTTP/2 protocol failures without matching generic errors', () => {
expect(isHttp2ProtocolError('net::ERR_HTTP2_PROTOCOL_ERROR')).toBe(true)
expect(isHttp2ProtocolError('Download failed: HTTP/2 protocol error')).toBe(true)
expect(isHttp2ProtocolError('Download failed: socket hang up')).toBe(false)
expect(isHttp2ProtocolError('HTTP proxy authentication failed')).toBe(false)
})
})
// ── Full update lifecycle through the store ──────────────────────────
describe('full update lifecycle through setUpdateStatus', () => {

View File

@ -7,7 +7,7 @@ import { useAppStore } from '../store'
import { Card } from './ui/card'
import { Button } from './ui/button'
import { Progress } from './ui/progress'
import { AlertCircle, Check, Loader2, Minus, X } from 'lucide-react'
import { AlertCircle, Check, Loader2, Minus, Network, RotateCw, X } from 'lucide-react'
import type { ChangelogData } from '../../../shared/types'
// ── Helpers ──────────────────────────────────────────────────────────
@ -26,13 +26,25 @@ function isAnimatedGif(url: string | undefined): boolean {
return typeof url === 'string' && url.toLowerCase().endsWith('.gif')
}
export function isHttp2ProtocolError(message: string): boolean {
const normalized = message.toLowerCase()
return (
normalized.includes('err_http2_protocol_error') ||
normalized.includes('http2_protocol_error') ||
(normalized.includes('http/2') && normalized.includes('protocol'))
)
}
type ErrorCardModel = {
variant?: 'default' | 'http1Compatibility'
title: string
summary: string
message: string
releaseUrl: string
primaryAction?: {
label: string
pendingLabel?: string
isPending?: boolean
onClick: () => void
}
}
@ -100,6 +112,8 @@ export function UpdateCard() {
const [mediaFailed, setMediaFailed] = useState(false)
const [mediaLoaded, setMediaLoaded] = useState(false)
const [installError, setInstallError] = useState<string | null>(null)
const [compatibilityRelaunching, setCompatibilityRelaunching] = useState(false)
const [compatibilitySetupError, setCompatibilitySetupError] = useState<string | null>(null)
// Why: the version-based dismiss gate at the bottom of the visibility
// section intentionally keeps error cards visible so a download failure
// still surfaces even if the user previously dismissed the "available"
@ -332,32 +346,61 @@ export function UpdateCard() {
})
}
const handleEnableHttp1Compatibility = () => {
setCompatibilityRelaunching(true)
setCompatibilitySetupError(null)
void window.api.settings
.set({ electronHttp1CompatibilityMode: true })
.then(() => window.api.app.relaunch())
.catch((error) => {
const message = String((error as Error)?.message ?? error)
console.error('[updates] failed to enable HTTP/1.1 compatibility:', error)
setCompatibilitySetupError(`Could not enable compatibility mode. ${message}`)
setCompatibilityRelaunching(false)
})
}
const isHttp2UpdateError = status.state === 'error' && isHttp2ProtocolError(status.message)
const errorCard: ErrorCardModel | null =
status.state === 'error'
? {
// Why: title is scoped to the operation that failed so check-time
// failures (commonly GitHub-side) don't read as a bug in Orca.
title: cachedVersion ? 'Update Error' : 'Update Check Failed',
summary: cachedVersion
? 'Could not complete the update.'
: 'Could not check for updates.',
message: status.message,
releaseUrl: releaseUrlForVersion(cachedVersion),
// Why: check-time failures are often transient (offline, GitHub
// hiccup), so offer a Re-check next to "Download Manually" instead
// of forcing the user into the manual fallback.
primaryAction: cachedVersion
? {
label: 'Retry Download',
onClick: handleUpdate
}
: {
label: 'Re-check',
onClick: () => {
void window.api.updater.check({ includePrerelease: false })
? isHttp2UpdateError
? {
variant: 'http1Compatibility',
title: 'HTTP/2 Download Blocked',
summary: 'Orca can retry through HTTP/1.1 compatibility mode.',
message: compatibilitySetupError ?? status.message,
releaseUrl: releaseUrlForVersion(cachedVersion),
primaryAction: {
label: 'Enable & Restart',
pendingLabel: 'Restarting...',
isPending: compatibilityRelaunching,
onClick: handleEnableHttp1Compatibility
}
}
: {
// Why: title is scoped to the operation that failed so check-time
// failures (commonly GitHub-side) don't read as a bug in Orca.
title: cachedVersion ? 'Update Error' : 'Update Check Failed',
summary: cachedVersion
? 'Could not complete the update.'
: 'Could not check for updates.',
message: status.message,
releaseUrl: releaseUrlForVersion(cachedVersion),
// Why: check-time failures are often transient (offline, GitHub
// hiccup), so offer a Re-check next to "Download Manually" instead
// of forcing the user into the manual fallback.
primaryAction: cachedVersion
? {
label: 'Retry Download',
onClick: handleUpdate
}
}
}
: {
label: 'Re-check',
onClick: () => {
void window.api.updater.check({ includePrerelease: false })
}
}
}
: installError
? {
title: 'Update Error',
@ -466,6 +509,7 @@ export function UpdateCard() {
summary={errorCard.summary}
message={errorCard.message}
releaseUrl={errorCard.releaseUrl}
variant={errorCard.variant}
primaryAction={errorCard.primaryAction}
onClose={handleCollapseWithAnimation}
/>
@ -831,6 +875,7 @@ function DownloadingContent({
// ── Error card content ───────────────────────────────────────────────
function ErrorCardContent({
variant = 'default',
title,
summary,
message,
@ -838,20 +883,31 @@ function ErrorCardContent({
primaryAction,
onClose
}: {
variant?: 'default' | 'http1Compatibility'
title: string
summary: string
message: string
releaseUrl: string
primaryAction?: {
label: string
pendingLabel?: string
isPending?: boolean
onClick: () => void
}
onClose: () => void
}) {
const isCompatibility = variant === 'http1Compatibility'
const Icon = isCompatibility ? Network : AlertCircle
return (
<div className="flex flex-col gap-3 p-4">
<div className="flex items-start justify-between gap-2">
<h3 className="text-sm font-semibold">{title}</h3>
<div className="flex items-start gap-3">
<div className="mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-md border border-border bg-muted/50 text-muted-foreground">
<Icon className="size-4" />
</div>
<div className="min-w-0 flex-1 space-y-1">
<h3 className="text-sm font-semibold">{title}</h3>
<p className="text-sm leading-relaxed text-muted-foreground">{summary}</p>
</div>
<Button
variant="ghost"
size="icon"
@ -863,14 +919,39 @@ function ErrorCardContent({
</Button>
</div>
<p className="text-sm text-muted-foreground">
{summary} {message}
</p>
{isCompatibility ? (
<div className="rounded-md border border-border/70 bg-muted/30 px-3 py-2">
<p className="text-xs leading-relaxed text-muted-foreground">
This turns on a process-wide Electron networking switch after restart. Use it for
corporate VPNs or proxies that reject HTTP/2 update downloads.
</p>
</div>
) : null}
<div className="rounded-md bg-muted/40 px-3 py-2">
<p className="mb-1 text-[11px] font-medium uppercase text-muted-foreground">Last error</p>
<p className="scrollbar-sleek max-h-20 overflow-auto break-words font-mono text-xs leading-relaxed text-muted-foreground">
{message}
</p>
</div>
<div className="flex gap-2">
{primaryAction && (
<Button variant="default" size="sm" onClick={primaryAction.onClick} className="flex-1">
{primaryAction.label}
<Button
variant="default"
size="sm"
onClick={primaryAction.onClick}
disabled={primaryAction.isPending}
className="flex-1 gap-1.5"
>
{primaryAction.isPending ? (
<Loader2 className="size-3.5 animate-spin" />
) : isCompatibility ? (
<RotateCw className="size-3.5" />
) : null}
{primaryAction.isPending && primaryAction.pendingLabel
? primaryAction.pendingLabel
: primaryAction.label}
</Button>
)}
<Button

View File

@ -0,0 +1,25 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import { getDefaultSettings } from '../../../../shared/constants'
import { AdvancedPane } from './AdvancedPane'
import { ADVANCED_SEARCH_ENTRY } from './advanced-search'
vi.mock('../../store', () => ({
useAppStore: (selector: (state: { settingsSearchQuery: string }) => unknown) =>
selector({ settingsSearchQuery: '' })
}))
describe('AdvancedPane', () => {
it('renders HTTP/1.1 compatibility as a neutral advanced setting', () => {
const markup = renderToStaticMarkup(
<AdvancedPane settings={getDefaultSettings('/tmp')} updateSettings={vi.fn()} />
)
expect(markup).toContain('Compatibility')
expect(markup).toContain('HTTP/1.1 Compatibility')
expect(markup).toContain('aria-checked="false"')
expect(markup).toContain('Explain HTTP/1.1 compatibility')
expect(ADVANCED_SEARCH_ENTRY.http1Compatibility.keywords).toContain('support')
expect(ADVANCED_SEARCH_ENTRY.http1Compatibility.keywords).toContain('troubleshooting')
})
})

View File

@ -0,0 +1,118 @@
import { useRef, useState } from 'react'
import { Info, Loader2, RotateCw } from 'lucide-react'
import type { GlobalSettings } from '../../../../shared/types'
import { useMountedRef } from '@/hooks/useMountedRef'
import { Button } from '../ui/button'
import { Label } from '../ui/label'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
import { SearchableSetting } from './SearchableSetting'
import { SettingsSubsectionHeader, SettingsSwitch } from './SettingsFormControls'
import { ADVANCED_PANE_SEARCH_ENTRIES, ADVANCED_SEARCH_ENTRY } from './advanced-search'
export { ADVANCED_PANE_SEARCH_ENTRIES }
type AdvancedPaneProps = {
settings: GlobalSettings
updateSettings: (updates: Partial<GlobalSettings>) => void
}
export function AdvancedPane({ settings, updateSettings }: AdvancedPaneProps): React.JSX.Element {
const mountedRef = useMountedRef()
const http1CompatibilityInitialRef = useRef(Boolean(settings.electronHttp1CompatibilityMode))
const [http1CompatibilityRelaunching, setHttp1CompatibilityRelaunching] = useState(false)
const http1CompatibilityEnabled = Boolean(settings.electronHttp1CompatibilityMode)
const http1CompatibilityRestartRequired =
http1CompatibilityEnabled !== http1CompatibilityInitialRef.current
const toggleHttp1CompatibilityMode = (): void => {
updateSettings({ electronHttp1CompatibilityMode: !http1CompatibilityEnabled })
}
const handleHttp1CompatibilityRelaunch = (): void => {
setHttp1CompatibilityRelaunching(true)
void window.api.app.relaunch().catch((error) => {
console.error('[settings] failed to relaunch for HTTP/1.1 compatibility:', error)
if (mountedRef.current) {
setHttp1CompatibilityRelaunching(false)
}
})
}
return (
<div className="space-y-4">
<section className="space-y-3">
<SettingsSubsectionHeader
title="Compatibility"
description="Low-level workarounds for support troubleshooting."
/>
<SearchableSetting
title={ADVANCED_SEARCH_ENTRY.http1Compatibility.title}
description={ADVANCED_SEARCH_ENTRY.http1Compatibility.description}
keywords={ADVANCED_SEARCH_ENTRY.http1Compatibility.keywords}
className="space-y-2 py-2"
id="advanced-http1-compatibility"
>
<div className="flex items-center justify-between gap-4">
<div className="min-w-0 shrink">
<div className="flex items-center gap-1.5">
<Label id="advanced-http1-compatibility-label">HTTP/1.1 Compatibility</Label>
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label="Explain HTTP/1.1 compatibility"
className="inline-flex size-6 items-center justify-center rounded-md text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<Info className="size-3.5" />
</button>
</TooltipTrigger>
<TooltipContent
side="top"
sideOffset={6}
className="max-w-[280px] leading-relaxed"
>
Use only when a corporate VPN or proxy breaks update downloads with HTTP/2
protocol errors. It affects all Electron networking after restart.
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
<SettingsSwitch
checked={http1CompatibilityEnabled}
onChange={toggleHttp1CompatibilityMode}
ariaLabelledBy="advanced-http1-compatibility-label"
/>
</div>
{http1CompatibilityRestartRequired ? (
<div className="flex items-center justify-between gap-3 rounded-md border border-border/50 bg-muted/30 px-3 py-2">
<div className="min-w-0">
<p className="text-xs font-medium">Restart required</p>
<p className="text-xs text-muted-foreground">
Orca applies this networking mode at startup.
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={handleHttp1CompatibilityRelaunch}
disabled={http1CompatibilityRelaunching}
className="shrink-0 gap-1.5"
>
{http1CompatibilityRelaunching ? (
<Loader2 className="size-3.5 animate-spin" />
) : (
<RotateCw className="size-3.5" />
)}
Restart
</Button>
</div>
) : null}
</SearchableSetting>
</section>
</div>
)
}

View File

@ -43,6 +43,7 @@ import { ComputerUsePane } from './ComputerUsePane'
import { MobileSettingsPane } from './MobileSettingsPane'
import { RuntimeEnvironmentsPane } from './RuntimeEnvironmentsPane'
import { PrivacyPane } from './PrivacyPane'
import { AdvancedPane } from './AdvancedPane'
import { SettingsSidebar } from './SettingsSidebar'
import { SettingsSetupGuideCard } from './SettingsSetupGuideCard'
import { ActiveSettingsSectionProvider, SettingsSection } from './SettingsSection'
@ -88,6 +89,7 @@ const SETTINGS_NAV_GROUPS = [
{ id: 'interface', title: 'Interface' },
{ id: 'remote', title: 'Remote Access' },
{ id: 'security', title: 'Privacy & Security' },
{ id: 'advanced', title: 'Advanced' },
{ id: 'experimental', title: 'Experimental' }
] as const
@ -1247,6 +1249,19 @@ function Settings(): React.JSX.Element {
{isSectionMounted('privacy') ? <PrivacyPane settings={settings} /> : null}
</SettingsSection>
{showDesktopOnlySettings ? (
<SettingsSection
id="advanced"
title="Advanced"
description="Low-level compatibility settings for troubleshooting."
searchEntries={getSectionSearchEntries('advanced')}
>
{isSectionMounted('advanced') ? (
<AdvancedPane settings={settings} updateSettings={updateSettings} />
) : null}
</SettingsSection>
) : null}
<SettingsSection
id="experimental"
title="Experimental"

View File

@ -0,0 +1,37 @@
import type { SettingsSearchEntry } from './settings-search'
export const ADVANCED_PANE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
{
title: 'HTTP/1.1 Compatibility',
description: 'Use HTTP/1.1 for Electron networking when HTTP/2 fails behind a proxy.',
keywords: [
'advanced',
'networking',
'network',
'http',
'http2',
'http/2',
'http1',
'http/1.1',
'compatibility',
'proxy',
'vpn',
'support',
'troubleshooting',
'updates',
'updater'
]
}
]
function findEntry(title: string): SettingsSearchEntry {
const entry = ADVANCED_PANE_SEARCH_ENTRIES.find((e) => e.title === title)
if (!entry) {
throw new Error(`Missing advanced-pane search entry: "${title}"`)
}
return entry
}
export const ADVANCED_SEARCH_ENTRY = {
http1Compatibility: findEntry('HTTP/1.1 Compatibility')
} as const

View File

@ -55,6 +55,7 @@ describe('settings navigation metadata', () => {
expect(webIds).not.toContain('mobile')
expect(webIds).not.toContain('computer-use')
expect(webIds).not.toContain('voice')
expect(webIds).not.toContain('advanced')
expect(webIds).toContain('servers')
expect(webIds).toContain('repo-repo-1')
})
@ -71,6 +72,14 @@ describe('settings navigation metadata', () => {
expect(sections.find((section) => section.id === 'voice')?.badge).toBeUndefined()
})
it('places Advanced near the bottom on desktop without putting it under Experimental', () => {
const desktopIds = ids()
expect(desktopIds).toContain('advanced')
expect(desktopIds.indexOf('advanced')).toBeLessThan(desktopIds.indexOf('experimental'))
expect(desktopIds.indexOf('privacy')).toBeLessThan(desktopIds.indexOf('advanced'))
})
it('keeps macOS permissions mac-only', () => {
expect(ids({ isMac: false })).not.toContain('developer-permissions')
expect(ids({ isMac: true })).toContain('developer-permissions')

View File

@ -27,7 +27,8 @@ import {
Smartphone,
SquareTerminal,
TextCursorInput,
UserCog
UserCog,
Wrench
} from 'lucide-react'
import type { Repo } from '../../../shared/types'
import { getRepoKindLabel } from '../../../shared/repo-kind'
@ -59,6 +60,7 @@ import { COMPUTER_USE_PANE_SEARCH_ENTRIES } from '@/components/settings/computer
import { VOICE_PANE_SEARCH_ENTRIES } from '@/components/settings/voice-pane-search'
import { DEVELOPER_PERMISSIONS_PANE_SEARCH_ENTRIES } from '@/components/settings/developer-permissions-search'
import { PRIVACY_PANE_SEARCH_ENTRIES } from '@/components/settings/privacy-search'
import { ADVANCED_PANE_SEARCH_ENTRIES } from '@/components/settings/advanced-search'
import { SHORTCUTS_PANE_SEARCH_ENTRIES } from '@/components/settings/shortcuts-search'
import { STATS_PANE_SEARCH_ENTRIES } from '@/components/stats/stats-search'
import { EXPERIMENTAL_PANE_SEARCH_ENTRIES } from '@/components/settings/experimental-search'
@ -305,6 +307,18 @@ export function buildSettingsNavigationMetadata({
searchEntries: PRIVACY_PANE_SEARCH_ENTRIES,
group: 'security'
},
...(showDesktopOnlySettings
? [
{
id: 'advanced',
title: 'Advanced',
description: 'Low-level compatibility settings for troubleshooting.',
icon: Wrench,
searchEntries: ADVANCED_PANE_SEARCH_ENTRIES,
group: 'advanced'
}
]
: []),
{
id: 'experimental',
title: 'Experimental',

View File

@ -19,6 +19,7 @@ export type SettingsNavTarget =
| 'computer-use'
| 'developer-permissions'
| 'privacy'
| 'advanced'
| 'voice'
| 'shortcuts'
| 'stats'

View File

@ -65,6 +65,7 @@ import {
} from '../../../../shared/workspace-statuses'
import { normalizeKagiSessionLink } from '../../../../shared/browser-url'
import type { OrcaHookScriptKind } from '../../lib/orca-hook-trust'
import type { SettingsNavTarget } from '@/lib/settings-navigation-types'
import {
filterSetupScriptPromptDismissalsToValidRepos,
getSetupScriptPromptDismissalKey
@ -605,32 +606,7 @@ export type UISlice = {
openSettingsPage: () => void
closeSettingsPage: () => void
settingsNavigationTarget: {
pane:
| 'general'
| 'integrations'
| 'accounts'
| 'browser'
| 'git'
| 'appearance'
| 'input'
| 'tasks'
| 'floating-workspace'
| 'terminal'
| 'quick-commands'
| 'notifications'
| 'computer-use'
| 'developer-permissions'
| 'privacy'
| 'shortcuts'
| 'stats'
| 'repo'
| 'agents'
| 'voice'
| 'experimental'
| 'orchestration'
| 'servers'
| 'mobile'
| 'ssh'
pane: SettingsNavTarget
repoId: string | null
sectionId?: string
intent?: 'add-quick-command'

View File

@ -231,6 +231,7 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
terminalScrollbackBytes: 10_000_000,
httpProxyUrl: '',
httpProxyBypassRules: '',
electronHttp1CompatibilityMode: false,
openLinksInApp: true,
openInApplications: [],
rightSidebarOpenByDefault: true,

View File

@ -2029,6 +2029,9 @@ export type GlobalSettings = {
httpProxyUrl?: string
/** Optional semicolon/comma/newline-separated bypass rules for httpProxyUrl. */
httpProxyBypassRules?: string
/** Why: corporate TLS-intercepting proxies can break Electron HTTP/2 downloads;
* this opt-in compatibility mode applies Chromium's process-wide HTTP/1.1 switch. */
electronHttp1CompatibilityMode?: boolean
/** Why: opening arbitrary links inside Orca uses an isolated guest browser surface.
* The setting stays opt-in so existing workflows continue to use the system browser
* until the user explicitly wants worktree-scoped in-app browsing. */