feat(settings): show each settings group as a titled card (#39253)

* feat(settings): group settings into titled cards so section boundaries are clear

Each settings group now shows its title above its own card, with hairlines
between rows, replacing one shared card where group headers looked like rows.
Adds SettingsGroup; SettingsSection drops its outer card when a pane is built
from groups, and SettingsRow/SearchableSetting mark rows for the dividers.

* fix(settings): use status-warning tokens for the task sources banner

* fix(settings): avoid subtree-restyling :has() spacing and keep orchestration search copy

* fix(settings): keep agent catalog rows mounted when an agent moves from available to installed

* fix(settings): hide groups that search empties and divide rows inside multi-row wrappers

* fix(settings): drop pane-wide :has scans, keep group headers when search empties a card

Ruling D2/E2/S3 on #39253 review: outermost rows own dividers, only an empty
card hides (direct-child selector) so header actions like Remove Project stay,
and panes declare grouped statically. Also restores the whole-header Alt-click
for the release channel, gives App Icon and the Windows admin row stable cards,
and removes wrapper divs that kept emptied cards visible.

* test(settings): Option-clicking the Updates version line reveals the release channel switcher

* test(settings): stub the updater API without a type assertion
This commit is contained in:
Brennan Benson
2026-11-07 12:37:16 -08:00
committed by GitHub
parent a299de5630
commit c3534a9954
129 changed files with 5248 additions and 5047 deletions
@@ -32,7 +32,7 @@ export function AgentDashboardExperimentalSetting({
'Kanban board for monitoring agents across worktrees, in-window or as a pop-out.'
)}
keywords={getExperimentalSearchEntry().agentDashboard.keywords}
className="space-y-3 py-2"
className="space-y-3 py-3"
id="experimental-agent-dashboard"
>
<div className="flex items-start justify-between gap-4">
@@ -385,7 +385,7 @@ export function AccountsPane({
return (
<div className="space-y-8">
{renderAccountsRemovalDialogs(model, removeCodexTarget, removeClaudeTarget)}
<SettingsSectionStack sections={visibleSections} spacing="group" />
<SettingsSectionStack sections={visibleSections} />
</div>
)
}
@@ -29,6 +29,7 @@ import {
cancelAccountAddition,
trackAccountAddition
} from '../sign-in/account-add-cancel-target'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
export function renderClaudeAccountsSection(model: AccountsPaneSectionModel): React.JSX.Element {
const {
@@ -78,317 +79,321 @@ export function renderClaudeAccountsSection(model: AccountsPaneSectionModel): Re
)
)
return (
<section key="claude-accounts" id="accounts-claude" className="space-y-4 scroll-mt-6">
<div className="space-y-1">
<h3 className="flex items-center gap-2 text-sm font-semibold">
<ClaudeIcon size={16} />
{translate('auto.components.settings.AccountsPane.26ef4b55be', 'Claude')}
</h3>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.AccountsPane.72b36ea174',
'Optional. Orca can use your normal Claude login; add accounts only if you want quick switching without moving chat sessions.'
)}
</p>
</div>
<SearchableSetting
title={translate('auto.components.settings.AccountsPane.8bbfd74556', 'Claude Accounts')}
<div key="claude-accounts" id="accounts-claude" className="scroll-mt-6">
<SettingsGroup
title={
<span className="flex items-center gap-2">
<ClaudeIcon size={16} />
{translate('auto.components.settings.AccountsPane.26ef4b55be', 'Claude')}
</span>
}
description={translate(
'accounts.claude.profileSwitching',
'Switching applies to the next Claude you start in any tab.'
'auto.components.settings.AccountsPane.72b36ea174',
'Optional. Orca can use your normal Claude login; add accounts only if you want quick switching without moving chat sessions.'
)}
keywords={['claude', 'account', 'rate limit', 'status bar', 'quota']}
className="space-y-3 py-2"
>
<div className="flex items-center justify-between gap-3">
<div className="space-y-0.5">
<Label>
{translate('auto.components.settings.AccountsPane.94d351af4a', 'Accounts')}
</Label>
<p className="text-xs text-muted-foreground">
{isRemoteAccountScope
? translate(
'accounts.remoteScopeAccountsAddHere',
'Showing accounts managed by {{value0}}. New accounts sign in there; you finish in this device’s browser.',
{ value0: accountRuntimeSentenceLabel }
)
: translate(
'auto.components.settings.AccountsPane.c0a52abfc5',
'Showing accounts for {{value0}}. New accounts are added there.',
{ value0: accountRuntimeSentenceLabel }
)}
</p>
</div>
<div className="flex shrink-0 items-center gap-1.5">
<ButtonGroup>
<Button
variant="outline"
size="xs"
onClick={() => void addClaudeAccount(false)}
disabled={addDisabled}
className="gap-1.5"
>
{claudeAction === 'adding' ? (
<Loader2 className="size-3 animate-spin" />
) : (
<Plus className="size-3" />
)}
{translate('auto.components.settings.AccountsPane.b0e948a4f9', 'Add Account')}
</Button>
{!isRemoteAccountScope && canOfferClaudeSignInLink(accountRuntime.runtime) ? (
<ClaudeSignInLinkMenu
variant="outline"
disabled={addDisabled}
onCopyLink={() => void addClaudeAccount(true)}
/>
) : null}
</ButtonGroup>
{/* Follow the running addition; hide Cancel only when its server prompt already offers it. */}
{claudeAction === 'adding' && !accountAdditionShownInPrompt('claude', accountOwner) ? (
<Button
variant="ghost"
size="xs"
onClick={() =>
void cancelAccountAddition('claude', () =>
window.api.claudeAccounts.cancelPendingLogin()
)
}
className="gap-1.5 text-muted-foreground hover:text-foreground"
>
<X className="size-3" />
{translate('auto.components.settings.AccountsPane.dbb9626ed1', 'Cancel')}
</Button>
) : null}
</div>
</div>
{remoteAccountScopeNotice}
{isRemoteAccountScope ? (
<HostAccountSignInPrompt
owner={accountOwner}
provider="claude"
hostLabel={accountRuntimeSentenceLabel}
/>
) : null}
<div className="space-y-2">
<button
type="button"
onClick={() =>
void runClaudeAccountAction('select:system', () =>
selectClaudeProviderAccount(accountOwner, {
accountId: null,
runtime: accountRuntime.runtime,
wslDistro: accountRuntime.wslDistro
})
)
}
disabled={claudeAction !== 'idle' || accountRuntimeUnavailable}
className={`flex w-full items-center justify-between gap-3 rounded-md border px-3 py-2.5 text-left transition-colors ${
systemClaudeActive
? 'border-foreground/20 bg-accent/15'
: 'border-border/70 hover:border-border hover:bg-accent/8'
} disabled:cursor-default disabled:opacity-100`}
>
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<div className="flex min-w-0 items-center gap-2">
<span className="truncate text-sm font-medium">
{systemDefaultEmail
? translate(
'accounts.claude.systemDefaultNamed',
'System default: {{value0}}',
{
value0: systemDefaultEmail
}
)
: translate(
'auto.components.settings.AccountsPane.f2a265f8c7',
'System default'
)}
</span>
{systemClaudeActive ? (
<Badge
variant="outline"
className="h-4 shrink-0 rounded px-1.5 text-[10px] font-medium leading-none text-foreground/80"
>
{translate('auto.components.settings.AccountsPane.e74831fb6b', 'Active')}
</Badge>
) : null}
</div>
<span className="truncate text-[11px] text-muted-foreground">
{translate(
'auto.components.settings.AccountsPane.e05d0ff737',
'Use your current {{value0}} Claude login.',
{ value0: accountRuntimeSentenceLabel }
)}
</span>
</div>
</button>
{systemDefaultIsSaved ? (
<div className="flex items-start justify-between gap-2">
<SearchableSetting
title={translate('auto.components.settings.AccountsPane.8bbfd74556', 'Claude Accounts')}
description={translate(
'accounts.claude.profileSwitching',
'Switching applies to the next Claude you start in any tab.'
)}
keywords={['claude', 'account', 'rate limit', 'status bar', 'quota']}
className="space-y-3 py-3"
>
<div className="flex items-center justify-between gap-3">
<div className="space-y-0.5">
<Label>
{translate('auto.components.settings.AccountsPane.94d351af4a', 'Accounts')}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
'accounts.claude.systemDefaultIsSaved',
'System default is signed in as {{value0}}, which is also a saved account. An earlier Orca version may have copied that login there. If it is not your own login, select System default and run claude /login.',
{ value0: systemDefaultEmail }
)}
{isRemoteAccountScope
? translate(
'accounts.remoteScopeAccountsAddHere',
'Showing accounts managed by {{value0}}. New accounts sign in there; you finish in this device’s browser.',
{ value0: accountRuntimeSentenceLabel }
)
: translate(
'auto.components.settings.AccountsPane.c0a52abfc5',
'Showing accounts for {{value0}}. New accounts are added there.',
{ value0: accountRuntimeSentenceLabel }
)}
</p>
<Button
variant="ghost"
size="xs"
onClick={() => updateSettings({ claudeCopiedSystemDefaultNoticeDismissed: true })}
>
{translate('accounts.claude.systemDefaultIsSavedDismiss', 'Dismiss')}
</Button>
</div>
<div className="flex shrink-0 items-center gap-1.5">
<ButtonGroup>
<Button
variant="outline"
size="xs"
onClick={() => void addClaudeAccount(false)}
disabled={addDisabled}
className="gap-1.5"
>
{claudeAction === 'adding' ? (
<Loader2 className="size-3 animate-spin" />
) : (
<Plus className="size-3" />
)}
{translate('auto.components.settings.AccountsPane.b0e948a4f9', 'Add Account')}
</Button>
{!isRemoteAccountScope && canOfferClaudeSignInLink(accountRuntime.runtime) ? (
<ClaudeSignInLinkMenu
variant="outline"
disabled={addDisabled}
onCopyLink={() => void addClaudeAccount(true)}
/>
) : null}
</ButtonGroup>
{/* Follow the running addition; hide Cancel only when its server prompt already offers it. */}
{claudeAction === 'adding' &&
!accountAdditionShownInPrompt('claude', accountOwner) ? (
<Button
variant="ghost"
size="xs"
onClick={() =>
void cancelAccountAddition('claude', () =>
window.api.claudeAccounts.cancelPendingLogin()
)
}
className="gap-1.5 text-muted-foreground hover:text-foreground"
>
<X className="size-3" />
{translate('auto.components.settings.AccountsPane.dbb9626ed1', 'Cancel')}
</Button>
) : null}
</div>
</div>
{remoteAccountScopeNotice}
{isRemoteAccountScope ? (
<HostAccountSignInPrompt
owner={accountOwner}
provider="claude"
hostLabel={accountRuntimeSentenceLabel}
/>
) : null}
{visibleClaudeAccounts.length === 0 ? (
<div className="rounded-md border border-dashed border-border/70 px-3 py-4 text-xs text-muted-foreground">
{isRemoteAccountScope
? translate(
'auto.components.settings.AccountsPane.remoteEmptyClaudeAccounts',
'No managed Claude accounts on {{value0}}. It uses its system default Claude login; add accounts on that server.',
{ value0: accountRuntimeSentenceLabel }
)
: translate(
'auto.components.settings.AccountsPane.3fe7862418',
"No managed Claude accounts for {{value0}}. Orca will use that environment's system default Claude login until you add one here.",
<div className="space-y-2">
<button
type="button"
onClick={() =>
void runClaudeAccountAction('select:system', () =>
selectClaudeProviderAccount(accountOwner, {
accountId: null,
runtime: accountRuntime.runtime,
wslDistro: accountRuntime.wslDistro
})
)
}
disabled={claudeAction !== 'idle' || accountRuntimeUnavailable}
className={`flex w-full items-center justify-between gap-3 rounded-md border px-3 py-2.5 text-left transition-colors ${
systemClaudeActive
? 'border-foreground/20 bg-accent/15'
: 'border-border/70 hover:border-border hover:bg-accent/8'
} disabled:cursor-default disabled:opacity-100`}
>
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<div className="flex min-w-0 items-center gap-2">
<span className="truncate text-sm font-medium">
{systemDefaultEmail
? translate(
'accounts.claude.systemDefaultNamed',
'System default: {{value0}}',
{
value0: systemDefaultEmail
}
)
: translate(
'auto.components.settings.AccountsPane.f2a265f8c7',
'System default'
)}
</span>
{systemClaudeActive ? (
<Badge
variant="outline"
className="h-4 shrink-0 rounded px-1.5 text-[10px] font-medium leading-none text-foreground/80"
>
{translate('auto.components.settings.AccountsPane.e74831fb6b', 'Active')}
</Badge>
) : null}
</div>
<span className="truncate text-[11px] text-muted-foreground">
{translate(
'auto.components.settings.AccountsPane.e05d0ff737',
'Use your current {{value0}} Claude login.',
{ value0: accountRuntimeSentenceLabel }
)}
</div>
) : (
visibleClaudeAccounts.map((account) => {
const isActive = providerAccountIsActiveInView(
account,
claudeAccounts,
accountRuntime,
accountVisibilityOptions
)
const isReauthing = claudeAction === `reauth:${account.id}`
const isBusy = claudeAction !== 'idle' || accountRuntimeUnavailable
const accountRuntimeView = getProviderAccountRuntime(account)
const reauthenticate = (copyLink: boolean): Promise<void> =>
runClaudeAccountAction(
`reauth:${account.id}`,
() =>
withCopiedClaudeLink(
copyLink,
window.api.claudeAccounts.reauthenticate({ accountId: account.id, copyLink })
),
accountRuntimeView
)
return (
<div
key={account.id}
className={`flex w-full items-center justify-between gap-3 rounded-md border px-3 py-2.5 text-left transition-colors ${
isActive
? 'border-foreground/20 bg-accent/15'
: 'border-border/70 hover:border-border hover:bg-accent/8'
}`}
</span>
</div>
</button>
{systemDefaultIsSaved ? (
<div className="flex items-start justify-between gap-2">
<p className="text-xs text-muted-foreground">
{translate(
'accounts.claude.systemDefaultIsSaved',
'System default is signed in as {{value0}}, which is also a saved account. An earlier Orca version may have copied that login there. If it is not your own login, select System default and run claude /login.',
{ value0: systemDefaultEmail }
)}
</p>
<Button
variant="ghost"
size="xs"
onClick={() => updateSettings({ claudeCopiedSystemDefaultNoticeDismissed: true })}
>
<div className="flex w-full items-center justify-between gap-3 max-md:flex-col max-md:items-start">
<button
type="button"
onClick={() => {
void runClaudeAccountAction(
`select:${account.id}`,
() =>
selectClaudeProviderAccount(accountOwner, {
accountId: account.id,
...accountRuntimeView
}),
accountRuntimeView
)
}}
disabled={isBusy || account.needsSignIn}
className="flex min-w-0 flex-1 flex-col gap-0.5 text-left disabled:cursor-default"
>
<div className="flex min-w-0 items-center gap-2">
<span className="truncate text-sm font-medium">{account.email}</span>
<Badge
variant="outline"
className="h-4 shrink-0 rounded px-1.5 text-[10px] font-medium leading-none text-foreground/70"
>
{getClaudeAccountRuntimeLabel(account, accountRuntime.label)}
</Badge>
{isActive ? (
{translate('accounts.claude.systemDefaultIsSavedDismiss', 'Dismiss')}
</Button>
</div>
) : null}
{visibleClaudeAccounts.length === 0 ? (
<div className="rounded-md border border-dashed border-border/70 px-3 py-4 text-xs text-muted-foreground">
{isRemoteAccountScope
? translate(
'auto.components.settings.AccountsPane.remoteEmptyClaudeAccounts',
'No managed Claude accounts on {{value0}}. It uses its system default Claude login; add accounts on that server.',
{ value0: accountRuntimeSentenceLabel }
)
: translate(
'auto.components.settings.AccountsPane.3fe7862418',
"No managed Claude accounts for {{value0}}. Orca will use that environment's system default Claude login until you add one here.",
{ value0: accountRuntimeSentenceLabel }
)}
</div>
) : (
visibleClaudeAccounts.map((account) => {
const isActive = providerAccountIsActiveInView(
account,
claudeAccounts,
accountRuntime,
accountVisibilityOptions
)
const isReauthing = claudeAction === `reauth:${account.id}`
const isBusy = claudeAction !== 'idle' || accountRuntimeUnavailable
const accountRuntimeView = getProviderAccountRuntime(account)
const reauthenticate = (copyLink: boolean): Promise<void> =>
runClaudeAccountAction(
`reauth:${account.id}`,
() =>
withCopiedClaudeLink(
copyLink,
window.api.claudeAccounts.reauthenticate({
accountId: account.id,
copyLink
})
),
accountRuntimeView
)
return (
<div
key={account.id}
className={`flex w-full items-center justify-between gap-3 rounded-md border px-3 py-2.5 text-left transition-colors ${
isActive
? 'border-foreground/20 bg-accent/15'
: 'border-border/70 hover:border-border hover:bg-accent/8'
}`}
>
<div className="flex w-full items-center justify-between gap-3 max-md:flex-col max-md:items-start">
<button
type="button"
onClick={() => {
void runClaudeAccountAction(
`select:${account.id}`,
() =>
selectClaudeProviderAccount(accountOwner, {
accountId: account.id,
...accountRuntimeView
}),
accountRuntimeView
)
}}
disabled={isBusy || account.needsSignIn}
className="flex min-w-0 flex-1 flex-col gap-0.5 text-left disabled:cursor-default"
>
<div className="flex min-w-0 items-center gap-2">
<span className="truncate text-sm font-medium">{account.email}</span>
<Badge
variant="outline"
className="h-4 shrink-0 rounded px-1.5 text-[10px] font-medium leading-none text-foreground/80"
className="h-4 shrink-0 rounded px-1.5 text-[10px] font-medium leading-none text-foreground/70"
>
{translate(
'auto.components.settings.AccountsPane.e74831fb6b',
'Active'
)}
{getClaudeAccountRuntimeLabel(account, accountRuntime.label)}
</Badge>
) : null}
</div>
<span className="truncate text-[11px] text-muted-foreground">
{account.needsSignIn
? translate(
'accounts.claude.signInRequired',
'Sign in again to use this account'
)
: account.organizationName
? `${account.organizationName} · ${formatAccountTimestamp(account.lastAuthenticatedAt)}`
: formatAccountTimestamp(account.lastAuthenticatedAt)}
</span>
</button>
<div className="flex shrink-0 items-center justify-end gap-1 max-md:w-full max-md:flex-wrap">
<ButtonGroup>
{isActive ? (
<Badge
variant="outline"
className="h-4 shrink-0 rounded px-1.5 text-[10px] font-medium leading-none text-foreground/80"
>
{translate(
'auto.components.settings.AccountsPane.e74831fb6b',
'Active'
)}
</Badge>
) : null}
</div>
<span className="truncate text-[11px] text-muted-foreground">
{account.needsSignIn
? translate(
'accounts.claude.signInRequired',
'Sign in again to use this account'
)
: account.organizationName
? `${account.organizationName} · ${formatAccountTimestamp(account.lastAuthenticatedAt)}`
: formatAccountTimestamp(account.lastAuthenticatedAt)}
</span>
</button>
<div className="flex shrink-0 items-center justify-end gap-1 max-md:w-full max-md:flex-wrap">
<ButtonGroup>
<Button
variant="ghost"
size="xs"
onClick={(event) => {
event.stopPropagation()
void reauthenticate(false)
}}
disabled={isRemoteAccountScope || isBusy}
className="h-6 px-2 text-muted-foreground hover:text-foreground"
>
{isReauthing ? (
<Loader2 className="size-3 animate-spin" />
) : (
<RefreshCw className="size-3" />
)}
{translate('accounts.claude.signInAgain', 'Sign in again')}
</Button>
{canOfferClaudeSignInLink(accountRuntimeView.runtime) ? (
<ClaudeSignInLinkMenu
variant="ghost"
disabled={isRemoteAccountScope || isBusy}
onCopyLink={() => void reauthenticate(true)}
/>
) : null}
</ButtonGroup>
<Button
variant="ghost"
size="xs"
onClick={(event) => {
event.stopPropagation()
void reauthenticate(false)
setRemoveClaudeTarget({
id: account.id,
runtime: getProviderAccountRuntime(account)
})
}}
disabled={isRemoteAccountScope || isBusy}
className="h-6 px-2 text-muted-foreground hover:text-foreground"
disabled={isBusy}
className="h-6 px-2 text-muted-foreground hover:text-destructive"
>
{isReauthing ? (
<Loader2 className="size-3 animate-spin" />
) : (
<RefreshCw className="size-3" />
)}
{translate('accounts.claude.signInAgain', 'Sign in again')}
<Trash2 className="size-3" />
{translate('auto.components.settings.AccountsPane.db209ee572', 'Remove')}
</Button>
{canOfferClaudeSignInLink(accountRuntimeView.runtime) ? (
<ClaudeSignInLinkMenu
variant="ghost"
disabled={isRemoteAccountScope || isBusy}
onCopyLink={() => void reauthenticate(true)}
/>
) : null}
</ButtonGroup>
<Button
variant="ghost"
size="xs"
onClick={(event) => {
event.stopPropagation()
setRemoveClaudeTarget({
id: account.id,
runtime: getProviderAccountRuntime(account)
})
}}
disabled={isBusy}
className="h-6 px-2 text-muted-foreground hover:text-destructive"
>
<Trash2 className="size-3" />
{translate('auto.components.settings.AccountsPane.db209ee572', 'Remove')}
</Button>
</div>
</div>
</div>
</div>
)
})
)}
</div>
</SearchableSetting>
{isRemoteAccountScope ? null : (
<ClaudeRunningSessionSwitchSetting settings={settings} updateSettings={updateSettings} />
)}
</section>
)
})
)}
</div>
</SearchableSetting>
{isRemoteAccountScope ? null : (
<ClaudeRunningSessionSwitchSetting settings={settings} updateSettings={updateSettings} />
)}
</SettingsGroup>
</div>
)
}
@@ -31,7 +31,7 @@ export function ClaudeRunningSessionSwitchSetting({
title={title}
description={description}
keywords={['claude', 'account', 'switch', 'restart', 'running', 'session']}
className="flex items-center justify-between gap-4 py-2"
className="flex items-center justify-between gap-4 py-3"
>
<div className="min-w-0 flex-1 space-y-0.5">
<Label>{title}</Label>
@@ -18,6 +18,7 @@ import {
cancelAccountAddition,
trackAccountAddition
} from '../sign-in/account-add-cancel-target'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
export function renderCodexAccountsSection(model: AccountsPaneSectionModel): React.JSX.Element {
const {
@@ -41,260 +42,275 @@ export function renderCodexAccountsSection(model: AccountsPaneSectionModel): Rea
wslCapabilitiesLoading
} = model
return (
<section key="codex-accounts" id="accounts-codex" className="space-y-4 scroll-mt-6">
<div className="space-y-1">
<h3 className="flex items-center gap-2 text-sm font-semibold">
<OpenAIIcon size={16} />
{translate('auto.components.settings.AccountsPane.ef91cfa06b', 'Codex')}
</h3>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.AccountsPane.cedfab35ab',
'Optional. Orca can use your normal Codex login; add accounts only if you want quick switching in Orca.'
)}
</p>
<p className="text-xs text-muted-foreground">
{isRemoteAccountScope
? translate(
'auto.components.settings.AccountsPane.remoteScopeAuthContext',
'Each account keeps its own sign-in context on {{value0}}.',
{ value0: accountRuntimeSentenceLabel }
)
: translate(
'auto.components.settings.AccountsPane.340d6f7a85',
'Each account keeps its own local sign-in context in Orca. Account auth stays on this device.'
<div key="codex-accounts" id="accounts-codex" className="scroll-mt-6">
<SettingsGroup
title={
<span className="flex items-center gap-2">
<OpenAIIcon size={16} />
{translate('auto.components.settings.AccountsPane.ef91cfa06b', 'Codex')}
</span>
}
description={
<>
<span className="block">
{translate(
'auto.components.settings.AccountsPane.cedfab35ab',
'Optional. Orca can use your normal Codex login; add accounts only if you want quick switching in Orca.'
)}
</p>
</div>
<SearchableSetting
title={translate('auto.components.settings.AccountsPane.3180536c7a', 'Codex Accounts')}
description={translate(
'auto.components.settings.AccountsPane.d0d53b7eb0',
'Manage which Codex account Orca uses for live rate limit fetching.'
)}
// Index every nested Codex control here so searches cannot render a matching header with an empty body.
keywords={getAccountsCodexSearchEntries().flatMap((entry) => [
entry.title,
entry.description ?? '',
...(entry.keywords ?? [])
])}
className="space-y-3 py-2"
>
{/* Keep a stable anchor for status-bar account-switcher deep links. */}
{activeCodexAuthWarning ? (
<div className="flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs text-destructive">
<AlertTriangle className="mt-0.5 size-3.5 shrink-0" />
<span>
{systemCodexMissingSignIn
? translate(
'auto.components.settings.AccountsPane.codexSystemDefaultNeedsSignIn',
'No Codex sign-in was found for {{value0}}.',
{ value0: accountRuntimeSentenceLabel }
)
: activeCodexAccountId
? translate(
'auto.components.settings.AccountsPane.75ca9b718e',
'Codex reported that the active account needs a fresh sign-in. Re-authenticate it before starting new Codex sessions.'
)
: translate(
'auto.components.settings.AccountsPane.e4a28e8894',
'Codex reported that the {{value0}} login needs a fresh sign-in. Sign in again before starting new Codex sessions.',
{ value0: accountRuntimeSentenceLabel }
)}
</span>
</div>
) : null}
{codexConfigSyncWarning ? (
<div className="flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs text-destructive">
<AlertTriangle className="mt-0.5 size-3.5 shrink-0" />
<span>
{codexConfigSyncWarning === 'managed-home-unavailable'
? translate(
'auto.components.settings.AccountsPane.codexConfigSyncManagedHomeUnavailable',
'Orca could not read this account’s Codex files just now, so settings may not be syncing. This usually clears on its own — antivirus or a backup tool briefly locks them.'
)
: codexConfigSyncWarning === 'missing-source'
? translate(
'auto.components.settings.AccountsPane.codexConfigSyncMissingSource',
'Codex is still using the settings it last synced because {{value0}} is missing. Restore that file to resume syncing.',
{ value0: codexConfigSync?.systemConfigPath ?? '' }
)
: codexConfigSyncWarning === 'blank-source'
? translate(
'auto.components.settings.AccountsPane.codexConfigSyncBlankSource',
'Codex is still using the settings it last synced because {{value0}} is empty. That is expected while a synced folder finishes downloading.',
{ value0: codexConfigSync?.systemConfigPath ?? '' }
)
: translate(
'auto.components.settings.AccountsPane.codexConfigSyncUnreadableSource',
"Codex is still using the settings it last synced because {{value0}} could not be read. Check that file's permissions.",
{ value0: codexConfigSync?.systemConfigPath ?? '' }
)}
</span>
</div>
) : null}
<div className="flex items-center justify-between gap-3">
<div className="space-y-0.5">
<Label>
{translate('auto.components.settings.AccountsPane.94d351af4a', 'Accounts')}
</Label>
<p className="text-xs text-muted-foreground">
<span className="block">
{isRemoteAccountScope
? translate(
'accounts.remoteScopeAccountsAddHere',
'Showing accounts managed by {{value0}}. New accounts sign in there; you finish in this device’s browser.',
'auto.components.settings.AccountsPane.remoteScopeAuthContext',
'Each account keeps its own sign-in context on {{value0}}.',
{ value0: accountRuntimeSentenceLabel }
)
: translate(
'auto.components.settings.AccountsPane.c0a52abfc5',
'Showing accounts for {{value0}}. New accounts are added there.',
{ value0: accountRuntimeSentenceLabel }
'auto.components.settings.AccountsPane.340d6f7a85',
'Each account keeps its own local sign-in context in Orca. Account auth stays on this device.'
)}
</p>
</div>
<div className="flex shrink-0 items-center gap-1.5">
<Button
variant="outline"
size="xs"
onClick={() =>
void runCodexAccountAction('adding', () =>
trackAccountAddition('codex', accountOwner, () =>
// Run remote login on the account owner rather than this device.
accountOwner.kind === 'environment'
? addCodexAccountOnHost(accountOwner)
: window.api.codexAccounts.add({
runtime: accountRuntime.runtime,
wslDistro: accountRuntime.wslDistro
})
)
)
}
disabled={
codexAction !== 'idle' || wslCapabilitiesLoading || accountRuntimeUnavailable
}
className="gap-1.5"
>
{codexAction === 'adding' ? (
<Loader2 className="size-3 animate-spin" />
) : (
<Plus className="size-3" />
)}
{translate('auto.components.settings.AccountsPane.b0e948a4f9', 'Add Account')}
</Button>
{/* Follow the running addition; hide Cancel only when its server prompt already offers it. */}
{codexAction === 'adding' && !accountAdditionShownInPrompt('codex', accountOwner) ? (
</span>
</>
}
>
<SearchableSetting
title={translate('auto.components.settings.AccountsPane.3180536c7a', 'Codex Accounts')}
description={translate(
'auto.components.settings.AccountsPane.d0d53b7eb0',
'Manage which Codex account Orca uses for live rate limit fetching.'
)}
// Index every nested Codex control here so searches cannot render a matching header with an empty body.
keywords={getAccountsCodexSearchEntries().flatMap((entry) => [
entry.title,
entry.description ?? '',
...(entry.keywords ?? [])
])}
className="space-y-3 py-3"
>
{/* Keep a stable anchor for status-bar account-switcher deep links. */}
{activeCodexAuthWarning ? (
<div className="flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs text-destructive">
<AlertTriangle className="mt-0.5 size-3.5 shrink-0" />
<span>
{systemCodexMissingSignIn
? translate(
'auto.components.settings.AccountsPane.codexSystemDefaultNeedsSignIn',
'No Codex sign-in was found for {{value0}}.',
{ value0: accountRuntimeSentenceLabel }
)
: activeCodexAccountId
? translate(
'auto.components.settings.AccountsPane.75ca9b718e',
'Codex reported that the active account needs a fresh sign-in. Re-authenticate it before starting new Codex sessions.'
)
: translate(
'auto.components.settings.AccountsPane.e4a28e8894',
'Codex reported that the {{value0}} login needs a fresh sign-in. Sign in again before starting new Codex sessions.',
{ value0: accountRuntimeSentenceLabel }
)}
</span>
</div>
) : null}
{codexConfigSyncWarning ? (
<div className="flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs text-destructive">
<AlertTriangle className="mt-0.5 size-3.5 shrink-0" />
<span>
{codexConfigSyncWarning === 'managed-home-unavailable'
? translate(
'auto.components.settings.AccountsPane.codexConfigSyncManagedHomeUnavailable',
'Orca could not read this account’s Codex files just now, so settings may not be syncing. This usually clears on its own — antivirus or a backup tool briefly locks them.'
)
: codexConfigSyncWarning === 'missing-source'
? translate(
'auto.components.settings.AccountsPane.codexConfigSyncMissingSource',
'Codex is still using the settings it last synced because {{value0}} is missing. Restore that file to resume syncing.',
{ value0: codexConfigSync?.systemConfigPath ?? '' }
)
: codexConfigSyncWarning === 'blank-source'
? translate(
'auto.components.settings.AccountsPane.codexConfigSyncBlankSource',
'Codex is still using the settings it last synced because {{value0}} is empty. That is expected while a synced folder finishes downloading.',
{ value0: codexConfigSync?.systemConfigPath ?? '' }
)
: translate(
'auto.components.settings.AccountsPane.codexConfigSyncUnreadableSource',
"Codex is still using the settings it last synced because {{value0}} could not be read. Check that file's permissions.",
{ value0: codexConfigSync?.systemConfigPath ?? '' }
)}
</span>
</div>
) : null}
<div className="flex items-center justify-between gap-3">
<div className="space-y-0.5">
<Label>
{translate('auto.components.settings.AccountsPane.94d351af4a', 'Accounts')}
</Label>
<p className="text-xs text-muted-foreground">
{isRemoteAccountScope
? translate(
'accounts.remoteScopeAccountsAddHere',
'Showing accounts managed by {{value0}}. New accounts sign in there; you finish in this device’s browser.',
{ value0: accountRuntimeSentenceLabel }
)
: translate(
'auto.components.settings.AccountsPane.c0a52abfc5',
'Showing accounts for {{value0}}. New accounts are added there.',
{ value0: accountRuntimeSentenceLabel }
)}
</p>
</div>
<div className="flex shrink-0 items-center gap-1.5">
<Button
variant="ghost"
variant="outline"
size="xs"
onClick={() =>
void cancelAccountAddition('codex', () =>
window.api.codexAccounts.cancelPendingLogin()
void runCodexAccountAction('adding', () =>
trackAccountAddition('codex', accountOwner, () =>
// Run remote login on the account owner rather than this device.
accountOwner.kind === 'environment'
? addCodexAccountOnHost(accountOwner)
: window.api.codexAccounts.add({
runtime: accountRuntime.runtime,
wslDistro: accountRuntime.wslDistro
})
)
)
}
disabled={
codexAction !== 'idle' || wslCapabilitiesLoading || accountRuntimeUnavailable
}
className="gap-1.5"
>
<X />
{translate('auto.components.settings.AccountsPane.dbb9626ed1', 'Cancel')}
{codexAction === 'adding' ? (
<Loader2 className="size-3 animate-spin" />
) : (
<Plus className="size-3" />
)}
{translate('auto.components.settings.AccountsPane.b0e948a4f9', 'Add Account')}
</Button>
) : null}
{/* Follow the running addition; hide Cancel only when its server prompt already offers it. */}
{codexAction === 'adding' && !accountAdditionShownInPrompt('codex', accountOwner) ? (
<Button
variant="ghost"
size="xs"
onClick={() =>
void cancelAccountAddition('codex', () =>
window.api.codexAccounts.cancelPendingLogin()
)
}
className="gap-1.5"
>
<X />
{translate('auto.components.settings.AccountsPane.dbb9626ed1', 'Cancel')}
</Button>
) : null}
</div>
</div>
</div>
{remoteAccountScopeNotice}
{/* Local login links cannot describe the selected server’s sign-in. */}
{isRemoteAccountScope ? (
<HostAccountSignInPrompt
owner={accountOwner}
provider="codex"
hostLabel={accountRuntimeSentenceLabel}
/>
) : (
<CodexLoginLinkNotice />
)}
<div className="space-y-2">
<button
type="button"
onClick={() =>
void runCodexAccountAction('select:system', () =>
selectCodexProviderAccount(accountOwner, {
accountId: null,
runtime: accountRuntime.runtime,
wslDistro: accountRuntime.wslDistro
})
)
}
disabled={codexAction !== 'idle' || accountRuntimeUnavailable}
className={`flex w-full items-center justify-between gap-3 rounded-md border px-3 py-2.5 text-left transition-colors ${
systemCodexNeedsSignIn
? 'border-destructive/50 bg-destructive/5'
: systemCodexActive
? 'border-foreground/20 bg-accent/15'
: 'border-border/70 hover:border-border hover:bg-accent/8'
} disabled:cursor-default disabled:opacity-100`}
>
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<div className="flex min-w-0 items-center gap-2">
<span className="truncate text-sm font-medium">
{translate('auto.components.settings.AccountsPane.f2a265f8c7', 'System default')}
</span>
{systemCodexActive ? (
<Badge
variant="outline"
className="h-4 shrink-0 rounded px-1.5 text-[10px] font-medium leading-none text-foreground/80"
>
{translate('auto.components.settings.AccountsPane.e74831fb6b', 'Active')}
</Badge>
) : null}
{systemCodexNeedsSignIn ? (
<Badge
variant="destructive"
className="h-4 shrink-0 rounded px-1.5 text-[10px] font-medium leading-none"
>
{translate('auto.components.settings.AccountsPane.93c47b333a', 'Needs sign-in')}
</Badge>
) : null}
</div>
<span
className={`truncate text-[11px] ${
systemCodexNeedsSignIn ? 'text-destructive' : 'text-muted-foreground'
}`}
>
{systemCodexNeedsSignIn
? systemCodexMissingSignIn
? translate(
'auto.components.settings.AccountsPane.codexSystemDefaultNeedsSignIn',
'No Codex sign-in was found for {{value0}}.',
{ value0: accountRuntimeSentenceLabel }
)
: translate(
'auto.components.settings.AccountsPane.fd62f37c24',
'Codex reported this {{value0}} login is out of date.',
{ value0: accountRuntimeSentenceLabel }
)
: getCodexSystemDefaultSubtitle(systemCodexIdentity, accountRuntimeSentenceLabel)}
</span>
</div>
</button>
{visibleCodexAccounts.length === 0 ? (
<div className="rounded-md border border-dashed border-border/70 px-3 py-4 text-xs text-muted-foreground">
{isRemoteAccountScope
? translate(
'auto.components.settings.AccountsPane.remoteEmptyCodexAccounts',
'No managed Codex accounts on {{value0}}. It uses its system default Codex login; add accounts on that server.',
{ value0: accountRuntimeSentenceLabel }
)
: translate(
'auto.components.settings.AccountsPane.b4c9450319',
"No managed Codex accounts for {{value0}}. Orca will use that environment's system default Codex login until you add one here.",
{ value0: accountRuntimeSentenceLabel }
)}
</div>
{remoteAccountScopeNotice}
{/* Local login links cannot describe the selected server’s sign-in. */}
{isRemoteAccountScope ? (
<HostAccountSignInPrompt
owner={accountOwner}
provider="codex"
hostLabel={accountRuntimeSentenceLabel}
/>
) : (
visibleCodexAccounts.map((account) => renderCodexAccountRow(account, model))
<CodexLoginLinkNotice />
)}
</div>
</SearchableSetting>
</section>
<div className="space-y-2">
<button
type="button"
onClick={() =>
void runCodexAccountAction('select:system', () =>
selectCodexProviderAccount(accountOwner, {
accountId: null,
runtime: accountRuntime.runtime,
wslDistro: accountRuntime.wslDistro
})
)
}
disabled={codexAction !== 'idle' || accountRuntimeUnavailable}
className={`flex w-full items-center justify-between gap-3 rounded-md border px-3 py-2.5 text-left transition-colors ${
systemCodexNeedsSignIn
? 'border-destructive/50 bg-destructive/5'
: systemCodexActive
? 'border-foreground/20 bg-accent/15'
: 'border-border/70 hover:border-border hover:bg-accent/8'
} disabled:cursor-default disabled:opacity-100`}
>
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<div className="flex min-w-0 items-center gap-2">
<span className="truncate text-sm font-medium">
{translate(
'auto.components.settings.AccountsPane.f2a265f8c7',
'System default'
)}
</span>
{systemCodexActive ? (
<Badge
variant="outline"
className="h-4 shrink-0 rounded px-1.5 text-[10px] font-medium leading-none text-foreground/80"
>
{translate('auto.components.settings.AccountsPane.e74831fb6b', 'Active')}
</Badge>
) : null}
{systemCodexNeedsSignIn ? (
<Badge
variant="destructive"
className="h-4 shrink-0 rounded px-1.5 text-[10px] font-medium leading-none"
>
{translate(
'auto.components.settings.AccountsPane.93c47b333a',
'Needs sign-in'
)}
</Badge>
) : null}
</div>
<span
className={`truncate text-[11px] ${
systemCodexNeedsSignIn ? 'text-destructive' : 'text-muted-foreground'
}`}
>
{systemCodexNeedsSignIn
? systemCodexMissingSignIn
? translate(
'auto.components.settings.AccountsPane.codexSystemDefaultNeedsSignIn',
'No Codex sign-in was found for {{value0}}.',
{ value0: accountRuntimeSentenceLabel }
)
: translate(
'auto.components.settings.AccountsPane.fd62f37c24',
'Codex reported this {{value0}} login is out of date.',
{ value0: accountRuntimeSentenceLabel }
)
: getCodexSystemDefaultSubtitle(
systemCodexIdentity,
accountRuntimeSentenceLabel
)}
</span>
</div>
</button>
{visibleCodexAccounts.length === 0 ? (
<div className="rounded-md border border-dashed border-border/70 px-3 py-4 text-xs text-muted-foreground">
{isRemoteAccountScope
? translate(
'auto.components.settings.AccountsPane.remoteEmptyCodexAccounts',
'No managed Codex accounts on {{value0}}. It uses its system default Codex login; add accounts on that server.',
{ value0: accountRuntimeSentenceLabel }
)
: translate(
'auto.components.settings.AccountsPane.b4c9450319',
"No managed Codex accounts for {{value0}}. Orca will use that environment's system default Codex login until you add one here.",
{ value0: accountRuntimeSentenceLabel }
)}
</div>
) : (
visibleCodexAccounts.map((account) => renderCodexAccountRow(account, model))
)}
</div>
</SearchableSetting>
</SettingsGroup>
</div>
)
}
@@ -25,6 +25,7 @@ import { openAppHttpLinkFromAnchor } from '@/lib/browser/navigation/app-http-lin
import { formatTimeAgo } from '../../../status-bar/tooltip'
import { ChatUnavailableOnWindowsNote } from '../../../agent/launch-settings/AgentChatChoiceControls'
import { structuredChatUnavailableOnPlatform } from '../../../../../../shared/agent-session/structured-chat-platform-support'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
function formatUsageReading(usage: ProviderRateLimits): string {
return translate(
@@ -182,191 +183,214 @@ export function AntigravityAccountsSection({
}
return (
<section id="accounts-antigravity" className="space-y-4 scroll-mt-6">
<h3 className="flex items-center gap-2 text-sm font-semibold">
<AgentIcon agent="antigravity" size={16} />
{translate('accounts.antigravity.title', 'Antigravity')}
</h3>
<p className="text-xs text-muted-foreground">
{translate(
'accounts.antigravity.signIn',
'Start agy on this host and complete its browser sign-in, then save the current account. To add a different account, use /logout in agy and sign in again.'
)}{' '}
<a
className="underline"
href="https://antigravity.google/docs/cli/install/"
target="_blank"
onClick={openAppHttpLinkFromAnchor}
rel="noopener noreferrer"
>
{translate('accounts.antigravity.docs', 'Sign-in instructions')}
</a>
</p>
{error && (
<p role="alert" className="text-xs text-destructive">
{error}
</p>
)}
{!state && error && (
<Button variant="outline" size="sm" disabled={busy} onClick={() => void run('List')}>
{translate('accounts.antigravity.retry', 'Retry')}
</Button>
)}
{state && (
<div className="space-y-3">
<p className="text-xs">
{state.currentAccount
? (state.currentAccount.email ??
translate('accounts.antigravity.identityUnknown', 'Signed-in identity unavailable'))
: translate('accounts.antigravity.signedOut', 'No native agy account is signed in.')}
<div id="accounts-antigravity" className="scroll-mt-6">
<SettingsGroup
title={
<span className="flex items-center gap-2">
<AgentIcon agent="antigravity" size={16} />
{translate('accounts.antigravity.title', 'Antigravity')}
</span>
}
>
<div className="space-y-4 py-4">
<p className="text-xs text-muted-foreground">
{translate(
'accounts.antigravity.signIn',
'Start agy on this host and complete its browser sign-in, then save the current account. To add a different account, use /logout in agy and sign in again.'
)}{' '}
<a
className="underline"
href="https://antigravity.google/docs/cli/install/"
target="_blank"
onClick={openAppHttpLinkFromAnchor}
rel="noopener noreferrer"
>
{translate('accounts.antigravity.docs', 'Sign-in instructions')}
</a>
</p>
{state.selectedAccountId && state.activeAccountId !== state.selectedAccountId && (
<p className="text-xs text-destructive">
{windowsHost
? translate(
'accounts.antigravity.windowsChanged',
'The host default account changed. Select a saved account to apply it again.'
)
: translate(
'accounts.antigravity.changed',
'The native account changed. Select a saved account again before launching agy.'
)}
{error && (
<p role="alert" className="text-xs text-destructive">
{error}
</p>
)}
<div className="flex flex-wrap items-center gap-2">
<Button
size="sm"
disabled={busy || !state.currentAccount}
onClick={() => void run('AddCurrent')}
>
{translate('accounts.antigravity.save', 'Save current account')}
</Button>
{!state && error && (
<Button variant="outline" size="sm" disabled={busy} onClick={() => void run('List')}>
{translate('accounts.antigravity.refresh', 'Refresh accounts')}
{translate('accounts.antigravity.retry', 'Retry')}
</Button>
<Button
variant="ghost"
size="sm"
disabled={busy || !state.currentAccount}
onClick={() => void run('Usage')}
>
{translate('accounts.antigravity.usage', 'Refresh usage')}
</Button>
{busy && (
<Loader2
className="size-4 animate-spin"
aria-label={translate('accounts.antigravity.working', 'Updating account')}
/>
)}
</div>
{usage && (
<p className="text-xs text-muted-foreground">
{usage.error ?? formatUsageReading(usage)}
</p>
)}
{state.accounts.map((account) => (
<div
key={account.id}
className="flex items-center justify-between gap-3 rounded-md border p-3"
>
<div className="space-y-1">
<p className="text-xs font-medium">
{account.email ?? translate('accounts.antigravity.saved', 'Saved Google account')}
{state && (
<div className="space-y-3">
<p className="text-xs">
{state.currentAccount
? (state.currentAccount.email ??
translate(
'accounts.antigravity.identityUnknown',
'Signed-in identity unavailable'
))
: translate(
'accounts.antigravity.signedOut',
'No native agy account is signed in.'
)}
</p>
{state.selectedAccountId && state.activeAccountId !== state.selectedAccountId && (
<p className="text-xs text-destructive">
{windowsHost
? translate(
'accounts.antigravity.windowsChanged',
'The host default account changed. Select a saved account to apply it again.'
)
: translate(
'accounts.antigravity.changed',
'The native account changed. Select a saved account again before launching agy.'
)}
</p>
{account.usage !== undefined && (
<p className="text-xs text-muted-foreground">
{account.usage
? translate(
'accounts.antigravity.lastReading',
'{{reading}} · Read {{time}}',
{
reading: formatUsageReading(account.usage),
time: formatTimeAgo(account.usage.updatedAt)
}
)
: translate('accounts.antigravity.noReading', 'Usage unknown: not read yet')}
</p>
)}
{state.activeAccountId === account.id && (
<Badge variant="secondary">
{translate('accounts.antigravity.nativeActive', 'Native account')}
</Badge>
)}
</div>
<div className="flex items-center gap-2">
<Button
size="xs"
variant="outline"
disabled={busy}
onClick={() => void run('Select', account.id)}
>
{state.selectedAccountId === account.id && state.activeAccountId === account.id
? translate('accounts.antigravity.selected', 'Selected')
: translate('accounts.antigravity.select', 'Select')}
</Button>
<Button
size="xs"
variant="ghost"
disabled={
busy ||
state.activeAccountId === account.id ||
state.selectedAccountId === account.id
}
onClick={() => void run('Remove', account.id)}
>
{translate('accounts.antigravity.remove', 'Remove')}
</Button>
</div>
</div>
))}
</div>
)}
<p className="text-xs text-muted-foreground">
{windowsHost
? translate(
'accounts.antigravity.windowsSessions',
'Select applies and verifies the host default agy credential. Command choices and later account changes are handled by agy. Existing sessions may keep their previous account.'
)
: translate(
'accounts.antigravity.sessions',
'Selection applies to new agy sessions on this host. Existing sessions may keep their previous account.'
)}
</p>
{chats && chatRuntime.kind === 'active' ? (
<AntigravityChatSignInRow
owner={owner}
label={label}
description={translate(
'accounts.antigravity.chatSeparate',
'Separate from your agy terminal account.'
)}
disabled={antigravityChatSignInBlocked(chatRuntime)}
runtime={
<ManagedRuntimeInstallRow
observation={chatRuntime}
agentLabel={translate('accounts.antigravity.title', 'Antigravity')}
signInRunning={antigravityChatSignInRunning(signIn.flow)}
buttonSize="default"
>
{(line, actions) => (
<div className="flex items-center justify-between gap-4">
<div className="min-w-0 flex-1">
<p className="text-sm font-medium">
{translate('settings.agents.chatRuntime', 'Chat runtime')}
</p>
<div className="mt-0.5">{line}</div>
</div>
<div className="flex shrink-0 items-center gap-1">{actions}</div>
</div>
)}
</ManagedRuntimeInstallRow>
}
/>
) : chats && structuredChatUnavailableOnPlatform('antigravity', ownerPlatform) ? (
<ChatUnavailableOnWindowsNote
label={translate('accounts.antigravity.title', 'Antigravity')}
/>
) : null}
</section>
<div className="flex flex-wrap items-center gap-2">
<Button
size="sm"
disabled={busy || !state.currentAccount}
onClick={() => void run('AddCurrent')}
>
{translate('accounts.antigravity.save', 'Save current account')}
</Button>
<Button
variant="outline"
size="sm"
disabled={busy}
onClick={() => void run('List')}
>
{translate('accounts.antigravity.refresh', 'Refresh accounts')}
</Button>
<Button
variant="ghost"
size="sm"
disabled={busy || !state.currentAccount}
onClick={() => void run('Usage')}
>
{translate('accounts.antigravity.usage', 'Refresh usage')}
</Button>
{busy && (
<Loader2
className="size-4 animate-spin"
aria-label={translate('accounts.antigravity.working', 'Updating account')}
/>
)}
</div>
{usage && (
<p className="text-xs text-muted-foreground">
{usage.error ?? formatUsageReading(usage)}
</p>
)}
{state.accounts.map((account) => (
<div
key={account.id}
className="flex items-center justify-between gap-3 rounded-md border p-3"
>
<div className="space-y-1">
<p className="text-xs font-medium">
{account.email ??
translate('accounts.antigravity.saved', 'Saved Google account')}
</p>
{account.usage !== undefined && (
<p className="text-xs text-muted-foreground">
{account.usage
? translate(
'accounts.antigravity.lastReading',
'{{reading}} · Read {{time}}',
{
reading: formatUsageReading(account.usage),
time: formatTimeAgo(account.usage.updatedAt)
}
)
: translate(
'accounts.antigravity.noReading',
'Usage unknown: not read yet'
)}
</p>
)}
{state.activeAccountId === account.id && (
<Badge variant="secondary">
{translate('accounts.antigravity.nativeActive', 'Native account')}
</Badge>
)}
</div>
<div className="flex items-center gap-2">
<Button
size="xs"
variant="outline"
disabled={busy}
onClick={() => void run('Select', account.id)}
>
{state.selectedAccountId === account.id &&
state.activeAccountId === account.id
? translate('accounts.antigravity.selected', 'Selected')
: translate('accounts.antigravity.select', 'Select')}
</Button>
<Button
size="xs"
variant="ghost"
disabled={
busy ||
state.activeAccountId === account.id ||
state.selectedAccountId === account.id
}
onClick={() => void run('Remove', account.id)}
>
{translate('accounts.antigravity.remove', 'Remove')}
</Button>
</div>
</div>
))}
</div>
)}
<p className="text-xs text-muted-foreground">
{windowsHost
? translate(
'accounts.antigravity.windowsSessions',
'Select applies and verifies the host default agy credential. Command choices and later account changes are handled by agy. Existing sessions may keep their previous account.'
)
: translate(
'accounts.antigravity.sessions',
'Selection applies to new agy sessions on this host. Existing sessions may keep their previous account.'
)}
</p>
{chats && chatRuntime.kind === 'active' ? (
<AntigravityChatSignInRow
owner={owner}
label={label}
description={translate(
'accounts.antigravity.chatSeparate',
'Separate from your agy terminal account.'
)}
disabled={antigravityChatSignInBlocked(chatRuntime)}
runtime={
<ManagedRuntimeInstallRow
observation={chatRuntime}
agentLabel={translate('accounts.antigravity.title', 'Antigravity')}
signInRunning={antigravityChatSignInRunning(signIn.flow)}
buttonSize="default"
>
{(line, actions) => (
<div className="flex items-center justify-between gap-4">
<div className="min-w-0 flex-1">
<p className="text-sm font-medium">
{translate('settings.agents.chatRuntime', 'Chat runtime')}
</p>
<div className="mt-0.5">{line}</div>
</div>
<div className="flex shrink-0 items-center gap-1">{actions}</div>
</div>
)}
</ManagedRuntimeInstallRow>
}
/>
) : chats && structuredChatUnavailableOnPlatform('antigravity', ownerPlatform) ? (
<ChatUnavailableOnWindowsNote
label={translate('accounts.antigravity.title', 'Antigravity')}
/>
) : null}
</div>
</SettingsGroup>
</div>
)
}
@@ -9,6 +9,7 @@ import { Button } from '../../../ui/button'
import type { CursorAccountStatus } from '../../../../../../shared/rate-limit-types'
import { SearchableSetting } from '../../section-layout/SearchableSetting'
import { openAppHttpLinkFromAnchor } from '@/lib/browser/navigation/app-http-link-open'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
const CURSOR_USAGE_DASHBOARD_URL = 'https://cursor.com/dashboard/spending'
const SEARCH_KEYWORDS = ['cursor', 'usage', 'plan', 'spend', 'billing', 'rate limit']
@@ -102,177 +103,177 @@ export function CursorAccountsSection(): React.JSX.Element {
: null
return (
<section id="accounts-cursor" className="space-y-4 scroll-mt-6">
<div className="flex items-start justify-between gap-3">
<div className="space-y-1">
<h3 className="flex items-center gap-2 text-sm font-semibold">
<div id="accounts-cursor" className="scroll-mt-6">
<SettingsGroup
title={
<span className="flex items-center gap-2">
<AgentIcon agent="cursor" size={16} />
{translate('auto.components.settings.CursorAccountsSection.title', 'Cursor')}
</h3>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.CursorAccountsSection.subtitle',
'Shows your Cursor plan usage from the sign-in already on this computer. Orca only reads it — it never changes your Cursor login.'
)}
</p>
</div>
<a
href={CURSOR_USAGE_DASHBOARD_URL}
target="_blank"
onClick={openAppHttpLinkFromAnchor}
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
>
{translate(
'auto.components.settings.CursorAccountsSection.dashboardLink',
'Cursor dashboard'
)}
<ExternalLink className="size-3" />
</a>
</div>
<div
className={cn(
'flex items-start gap-3 rounded-lg border bg-muted/20 p-3',
signedIn && tokenFresh ? 'border-border/60' : 'border-border/40'
</span>
}
description={translate(
'auto.components.settings.CursorAccountsSection.subtitle',
'Shows your Cursor plan usage from the sign-in already on this computer. Orca only reads it — it never changes your Cursor login.'
)}
action={
<a
href={CURSOR_USAGE_DASHBOARD_URL}
target="_blank"
onClick={openAppHttpLinkFromAnchor}
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
>
{translate(
'auto.components.settings.CursorAccountsSection.dashboardLink',
'Cursor dashboard'
)}
<ExternalLink className="size-3" />
</a>
}
>
<ShieldCheck
className={cn(
'mt-0.5 size-4 shrink-0',
signedIn && tokenFresh ? 'text-foreground' : 'text-muted-foreground'
)}
/>
<div className="min-w-0 flex-1 space-y-1">
{loading ? (
<p className="text-xs text-muted-foreground">
{translate('auto.components.settings.CursorAccountsSection.loading', 'Loading…')}
</p>
) : signedIn ? (
<>
<p className="truncate text-xs font-medium">
{status?.email ??
status?.displayName ??
translate('auto.components.settings.CursorAccountsSection.signedIn', 'Signed in')}
</p>
<div className="flex items-start gap-3 py-3">
<ShieldCheck
className={cn(
'mt-0.5 size-4 shrink-0',
signedIn && tokenFresh ? 'text-foreground' : 'text-muted-foreground'
)}
/>
<div className="min-w-0 flex-1 space-y-1">
{loading ? (
<p className="text-xs text-muted-foreground">
{tokenFresh
? sourceLabel
? translate(
'auto.components.settings.CursorAccountsSection.signedInFrom',
'Signed in. Orca reads the session stored in {{source}}.',
{ source: sourceLabel }
)
: translate(
'auto.components.settings.CursorAccountsSection.signedInGeneric',
'Signed in. Orca reads the Cursor session stored on this computer.'
)
: translate(
'auto.components.settings.CursorAccountsSection.expired',
'Sign-in expired — run cursor-agent login on the computer running Orca, then click Refresh usage.'
{translate('auto.components.settings.CursorAccountsSection.loading', 'Loading…')}
</p>
) : signedIn ? (
<>
<p className="truncate text-xs font-medium">
{status?.email ??
status?.displayName ??
translate(
'auto.components.settings.CursorAccountsSection.signedIn',
'Signed in'
)}
</p>
</>
) : (
<>
<p className="text-xs font-medium">
{translate(
'auto.components.settings.CursorAccountsSection.signedOut',
'No Cursor sign-in found on this computer'
)}
</p>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.CursorAccountsSection.signedOutHelp',
'Sign in with Cursor IDE, or run cursor-agent login in a terminal, then click Refresh usage here.'
)}
</p>
</>
)}
{status?.error ? <p className="text-xs text-destructive">{status.error}</p> : null}
</div>
<Button
variant="outline"
size="xs"
disabled={refreshing}
onClick={() => void handleRefreshUsage()}
className="shrink-0"
>
{refreshing ? (
<Loader2 className="size-3 animate-spin" />
) : (
<RefreshCw className="size-3" />
)}
{translate(
'auto.components.settings.CursorAccountsSection.refreshUsage',
'Refresh usage'
)}
</Button>
</div>
{pools.length > 0 || monthly ? (
<SearchableSetting
title={translate(
'auto.components.settings.CursorAccountsSection.usageTitle',
'Plan usage'
)}
description={translate(
'auto.components.settings.CursorAccountsSection.usageDescription',
'Cursor bills two pools that reset with your billing cycle, plus on-demand spend once they run out.'
)}
keywords={SEARCH_KEYWORDS}
>
<div className="space-y-1">
{pools.map((pool) => (
<div key={pool.name} className="flex items-center gap-2 text-xs">
<Badge variant="secondary">
<span className="tabular-nums">{Math.round(pool.usedPercent)}%</span>
</Badge>
<span className="text-muted-foreground">{pool.name}</span>
</div>
))}
{monthly ? (
<div className="flex items-center gap-2 text-xs">
<Badge variant="secondary">
<span className="tabular-nums">{Math.round(monthly.usedPercent)}%</span>
</Badge>
<span className="text-muted-foreground">
{translate('auto.components.settings.CursorAccountsSection.planTotal', 'Plan')}
</span>
</div>
) : null}
{(monthly ?? pools[0])?.resetDescription ? (
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.CursorAccountsSection.resets',
'Resets {{when}}',
{ when: (monthly ?? pools[0])?.resetDescription ?? '' }
)}
</p>
) : null}
{staleUsageError ? (
<p className="text-xs text-destructive">
{translate(
'auto.components.settings.CursorAccountsSection.staleUsage',
'Last known usage — the latest refresh failed: {{reason}}',
{ reason: staleUsageError }
)}
</p>
) : null}
</p>
<p className="text-xs text-muted-foreground">
{tokenFresh
? sourceLabel
? translate(
'auto.components.settings.CursorAccountsSection.signedInFrom',
'Signed in. Orca reads the session stored in {{source}}.',
{ source: sourceLabel }
)
: translate(
'auto.components.settings.CursorAccountsSection.signedInGeneric',
'Signed in. Orca reads the Cursor session stored on this computer.'
)
: translate(
'auto.components.settings.CursorAccountsSection.expired',
'Sign-in expired — run cursor-agent login on the computer running Orca, then click Refresh usage.'
)}
</p>
</>
) : (
<>
<p className="text-xs font-medium">
{translate(
'auto.components.settings.CursorAccountsSection.signedOut',
'No Cursor sign-in found on this computer'
)}
</p>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.CursorAccountsSection.signedOutHelp',
'Sign in with Cursor IDE, or run cursor-agent login in a terminal, then click Refresh usage here.'
)}
</p>
</>
)}
{status?.error ? <p className="text-xs text-destructive">{status.error}</p> : null}
</div>
</SearchableSetting>
) : unavailableReason ? (
<SearchableSetting
title={translate('auto.components.settings.CursorAccountsSection.usageLabel', 'Usage')}
description={translate(
'auto.components.settings.CursorAccountsSection.noAllowance',
'Cursor reported no usage allowance for this account.'
)}
keywords={SEARCH_KEYWORDS}
>
<p className="text-xs text-muted-foreground">{unavailableReason}</p>
</SearchableSetting>
) : null}
</section>
<Button
variant="outline"
size="xs"
disabled={refreshing}
onClick={() => void handleRefreshUsage()}
className="shrink-0"
>
{refreshing ? (
<Loader2 className="size-3 animate-spin" />
) : (
<RefreshCw className="size-3" />
)}
{translate(
'auto.components.settings.CursorAccountsSection.refreshUsage',
'Refresh usage'
)}
</Button>
</div>
{pools.length > 0 || monthly ? (
<SearchableSetting
title={translate(
'auto.components.settings.CursorAccountsSection.usageTitle',
'Plan usage'
)}
description={translate(
'auto.components.settings.CursorAccountsSection.usageDescription',
'Cursor bills two pools that reset with your billing cycle, plus on-demand spend once they run out.'
)}
keywords={SEARCH_KEYWORDS}
className="py-3"
>
<div className="space-y-1">
{pools.map((pool) => (
<div key={pool.name} className="flex items-center gap-2 text-xs">
<Badge variant="secondary">
<span className="tabular-nums">{Math.round(pool.usedPercent)}%</span>
</Badge>
<span className="text-muted-foreground">{pool.name}</span>
</div>
))}
{monthly ? (
<div className="flex items-center gap-2 text-xs">
<Badge variant="secondary">
<span className="tabular-nums">{Math.round(monthly.usedPercent)}%</span>
</Badge>
<span className="text-muted-foreground">
{translate('auto.components.settings.CursorAccountsSection.planTotal', 'Plan')}
</span>
</div>
) : null}
{(monthly ?? pools[0])?.resetDescription ? (
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.CursorAccountsSection.resets',
'Resets {{when}}',
{ when: (monthly ?? pools[0])?.resetDescription ?? '' }
)}
</p>
) : null}
{staleUsageError ? (
<p className="text-xs text-destructive">
{translate(
'auto.components.settings.CursorAccountsSection.staleUsage',
'Last known usage — the latest refresh failed: {{reason}}',
{ reason: staleUsageError }
)}
</p>
) : null}
</div>
</SearchableSetting>
) : unavailableReason ? (
<SearchableSetting
title={translate('auto.components.settings.CursorAccountsSection.usageLabel', 'Usage')}
description={translate(
'auto.components.settings.CursorAccountsSection.noAllowance',
'Cursor reported no usage allowance for this account.'
)}
keywords={SEARCH_KEYWORDS}
className="py-3"
>
<p className="text-xs text-muted-foreground">{unavailableReason}</p>
</SearchableSetting>
) : null}
</SettingsGroup>
</div>
)
}
@@ -9,6 +9,7 @@ import { Button } from '../../../ui/button'
import type { GrokAccountStatus } from '../../../../../../shared/rate-limit-types'
import { SearchableSetting } from '../../section-layout/SearchableSetting'
import { openAppHttpLinkFromAnchor } from '@/lib/browser/navigation/app-http-link-open'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
const GROK_CLI_DOCS_URL = 'https://docs.x.ai/build/overview'
export function GrokAccountsSection(): React.JSX.Element {
@@ -63,159 +64,159 @@ export function GrokAccountsSection(): React.JSX.Element {
: null
return (
<section id="accounts-grok" className="space-y-4 scroll-mt-6">
<div className="flex items-start justify-between gap-3">
<div className="space-y-1">
<h3 className="flex items-center gap-2 text-sm font-semibold">
<div id="accounts-grok" className="scroll-mt-6">
<SettingsGroup
title={
<span className="flex items-center gap-2">
<AgentIcon agent="grok" size={16} />
{translate('auto.components.settings.GrokAccountsSection.a1b2c3d4e5', 'Grok (xAI)')}
</h3>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.GrokAccountsSection.f6e5d4c3b2',
'Shows weekly credit usage from your Grok CLI sign-in (session file ~/.grok/auth.json).'
)}
</p>
</div>
<a
href={GROK_CLI_DOCS_URL}
target="_blank"
onClick={openAppHttpLinkFromAnchor}
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
>
{translate('auto.components.settings.GrokAccountsSection.0d8e77bc40', 'Grok CLI docs')}
<ExternalLink className="size-3" />
</a>
</div>
<div
className={cn(
'flex items-start gap-3 rounded-lg border bg-muted/20 p-3',
signedIn && tokenFresh ? 'border-border/60' : 'border-border/40'
</span>
}
description={translate(
'auto.components.settings.GrokAccountsSection.f6e5d4c3b2',
'Shows weekly credit usage from your Grok CLI sign-in (session file ~/.grok/auth.json).'
)}
action={
<a
href={GROK_CLI_DOCS_URL}
target="_blank"
onClick={openAppHttpLinkFromAnchor}
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
>
{translate('auto.components.settings.GrokAccountsSection.0d8e77bc40', 'Grok CLI docs')}
<ExternalLink className="size-3" />
</a>
}
>
<ShieldCheck
className={cn(
'mt-0.5 size-4 shrink-0',
signedIn && tokenFresh ? 'text-foreground' : 'text-muted-foreground'
)}
/>
<div className="min-w-0 flex-1 space-y-1">
{loading ? (
<p className="text-xs text-muted-foreground">
{translate('auto.components.settings.GrokAccountsSection.ad47a33f72', 'Loading…')}
</p>
) : signedIn ? (
<>
<p className="truncate text-xs font-medium">
{status?.email ??
translate('auto.components.settings.GrokAccountsSection.b2c3d4e5f6', 'Signed in')}
</p>
<div className="flex items-start gap-3 py-3">
<ShieldCheck
className={cn(
'mt-0.5 size-4 shrink-0',
signedIn && tokenFresh ? 'text-foreground' : 'text-muted-foreground'
)}
/>
<div className="min-w-0 flex-1 space-y-1">
{loading ? (
<p className="text-xs text-muted-foreground">
{tokenFresh
? translate(
'auto.components.settings.GrokAccountsSection.b36fa2c908',
'Signed in. Orca reads the Grok CLI session stored on disk.'
)
: translate(
'auto.components.settings.GrokAccountsSection.f08c41de73',
'Session expired — run grok on the computer running Orca and wait for it to start. If prompted, complete sign-in, then click Refresh usage. No chat message is needed.'
{translate('auto.components.settings.GrokAccountsSection.ad47a33f72', 'Loading…')}
</p>
) : signedIn ? (
<>
<p className="truncate text-xs font-medium">
{status?.email ??
translate(
'auto.components.settings.GrokAccountsSection.b2c3d4e5f6',
'Signed in'
)}
</p>
</>
) : (
<>
<p className="text-xs font-medium">
{translate(
'auto.components.settings.GrokAccountsSection.e5f6a7b8c9',
'Not signed in to Grok CLI'
)}
</p>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.GrokAccountsSection.f6a7b8c9d0',
'In a terminal, run grok login, then click Refresh usage here.'
)}
</p>
</>
)}
{status?.error ? <p className="text-xs text-destructive">{status.error}</p> : null}
</div>
<Button
variant="outline"
size="xs"
disabled={refreshing}
onClick={() => void handleRefreshUsage()}
className="shrink-0 gap-1"
>
{refreshing ? (
<Loader2 className="size-3 animate-spin" />
) : (
<RefreshCw className="size-3" />
)}
{translate('auto.components.settings.GrokAccountsSection.3325d996cb', 'Refresh usage')}
</Button>
</div>
{usageWindow ? (
<SearchableSetting
title={
usageIsWeekly
? translate(
'auto.components.settings.GrokAccountsSection.a8f3e2c1b4',
'Weekly credits'
)
: translate(
'auto.components.settings.GrokAccountsSection.e6dadc1e2b',
'Monthly usage'
)
}
description={
usageIsWeekly
? translate(
'auto.components.settings.GrokAccountsSection.b7e2d9f0a3',
'Same weekly credit % as the grok /usage screen in the terminal.'
)
: translate(
'auto.components.settings.GrokAccountsSection.75e396bf42',
'Included monthly usage for Grok unified-billing accounts.'
)
}
keywords={['grok', 'xai', 'usage', 'credits', 'oauth']}
>
<div className="flex items-center gap-2 text-xs">
<Badge variant="secondary" className="tabular-nums">
{Math.round(usageWindow.usedPercent)}%
</Badge>
{usageWindow.resetDescription ? (
<span className="text-muted-foreground">
{translate(
'auto.components.settings.GrokAccountsSection.c6d1a8f4e2',
'Resets {{when}}',
{ when: usageWindow.resetDescription }
)}
</span>
) : null}
{grokUsage?.usageMetadata?.authProvenance ? (
<span className="truncate text-muted-foreground">
{grokUsage.usageMetadata.authProvenance}
</span>
) : null}
</p>
<p className="text-xs text-muted-foreground">
{tokenFresh
? translate(
'auto.components.settings.GrokAccountsSection.b36fa2c908',
'Signed in. Orca reads the Grok CLI session stored on disk.'
)
: translate(
'auto.components.settings.GrokAccountsSection.f08c41de73',
'Session expired — run grok on the computer running Orca and wait for it to start. If prompted, complete sign-in, then click Refresh usage. No chat message is needed.'
)}
</p>
</>
) : (
<>
<p className="text-xs font-medium">
{translate(
'auto.components.settings.GrokAccountsSection.e5f6a7b8c9',
'Not signed in to Grok CLI'
)}
</p>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.GrokAccountsSection.f6a7b8c9d0',
'In a terminal, run grok login, then click Refresh usage here.'
)}
</p>
</>
)}
{status?.error ? <p className="text-xs text-destructive">{status.error}</p> : null}
</div>
</SearchableSetting>
) : unavailableReason ? (
<SearchableSetting
title={translate('auto.components.settings.GrokAccountsSection.0bb18642b7', 'Usage')}
description={translate(
'auto.components.settings.GrokAccountsSection.a8f4139350',
'Grok reported no usage percentage for this account.'
)}
keywords={['grok', 'xai', 'usage', 'credits', 'oauth']}
>
<p className="text-xs text-muted-foreground">{unavailableReason}</p>
</SearchableSetting>
) : null}
</section>
<Button
variant="outline"
size="xs"
disabled={refreshing}
onClick={() => void handleRefreshUsage()}
className="shrink-0 gap-1"
>
{refreshing ? (
<Loader2 className="size-3 animate-spin" />
) : (
<RefreshCw className="size-3" />
)}
{translate('auto.components.settings.GrokAccountsSection.3325d996cb', 'Refresh usage')}
</Button>
</div>
{usageWindow ? (
<SearchableSetting
title={
usageIsWeekly
? translate(
'auto.components.settings.GrokAccountsSection.a8f3e2c1b4',
'Weekly credits'
)
: translate(
'auto.components.settings.GrokAccountsSection.e6dadc1e2b',
'Monthly usage'
)
}
description={
usageIsWeekly
? translate(
'auto.components.settings.GrokAccountsSection.b7e2d9f0a3',
'Same weekly credit % as the grok /usage screen in the terminal.'
)
: translate(
'auto.components.settings.GrokAccountsSection.75e396bf42',
'Included monthly usage for Grok unified-billing accounts.'
)
}
keywords={['grok', 'xai', 'usage', 'credits', 'oauth']}
className="py-3"
>
<div className="flex items-center gap-2 text-xs">
<Badge variant="secondary" className="tabular-nums">
{Math.round(usageWindow.usedPercent)}%
</Badge>
{usageWindow.resetDescription ? (
<span className="text-muted-foreground">
{translate(
'auto.components.settings.GrokAccountsSection.c6d1a8f4e2',
'Resets {{when}}',
{ when: usageWindow.resetDescription }
)}
</span>
) : null}
{grokUsage?.usageMetadata?.authProvenance ? (
<span className="truncate text-muted-foreground">
{grokUsage.usageMetadata.authProvenance}
</span>
) : null}
</div>
</SearchableSetting>
) : unavailableReason ? (
<SearchableSetting
title={translate('auto.components.settings.GrokAccountsSection.0bb18642b7', 'Usage')}
description={translate(
'auto.components.settings.GrokAccountsSection.a8f4139350',
'Grok reported no usage percentage for this account.'
)}
keywords={['grok', 'xai', 'usage', 'credits', 'oauth']}
className="py-3"
>
<p className="text-xs text-muted-foreground">{unavailableReason}</p>
</SearchableSetting>
) : null}
</SettingsGroup>
</div>
)
}
@@ -16,6 +16,7 @@ import {
DialogDescription,
DialogFooter
} from '../../../ui/dialog'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
type Snapshot = { opencode?: ManagedDataAccountsState; devin?: ManagedDataAccountsState }
@@ -86,61 +87,62 @@ export function ManagedDataAccountsSection({
const command = `orca account add --agent ${provider}`
return (
<section className="space-y-3">
<h3 className="text-sm font-semibold">
{provider === 'opencode'
<SettingsGroup
title={
provider === 'opencode'
? translate('auto.lib.agent.catalog.e7a4ca5103', 'OpenCode')
: translate('auto.lib.agent.catalog.fc80296033', 'Devin')}
</h3>
<p className="text-xs text-muted-foreground">
{translate(
'accounts.managedData.description',
'Add accounts by running this command in a terminal on the Orca host. Selection applies to new explicit agent launches on that host; direct SSH relay and Windows-hosted WSL launches use their own credentials.'
: translate('auto.lib.agent.catalog.fc80296033', 'Devin')
}
description={translate(
'accounts.managedData.description',
'Add accounts by running this command in a terminal on the Orca host. Selection applies to new explicit agent launches on that host; direct SSH relay and Windows-hosted WSL launches use their own credentials.'
)}
>
<div className="space-y-3 py-3">
<code className="text-xs">{command}</code>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => {
void window.api.ui
.writeClipboardText(command)
.then(() => setCopied(true))
.catch((cause: unknown) => setError(String(cause)))
}}
>
{copied
? translate('accounts.managedData.copied', 'Copied')
: translate('accounts.managedData.add', 'Copy add account command')}
</Button>
<Button
variant="ghost"
size="sm"
disabled={busy}
onClick={() => {
void refresh()
}}
>
{translate('accounts.managedData.refresh', 'Refresh accounts')}
</Button>
</div>
{!state && !error && (
<p className="text-xs text-muted-foreground">
{translate(
'accounts.managedData.upgrade',
'If accounts do not appear, update or restart the Orca host.'
)}
</p>
)}
{error && (
<p role="alert" className="text-xs text-destructive">
{error}
</p>
)}
</p>
<code className="text-xs">{command}</code>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => {
void window.api.ui
.writeClipboardText(command)
.then(() => setCopied(true))
.catch((cause: unknown) => setError(String(cause)))
}}
>
{copied
? translate('accounts.managedData.copied', 'Copied')
: translate('accounts.managedData.add', 'Copy add account command')}
</Button>
<Button
variant="ghost"
size="sm"
disabled={busy}
onClick={() => {
void refresh()
}}
>
{translate('accounts.managedData.refresh', 'Refresh accounts')}
</Button>
</div>
{!state && !error && (
<p className="text-xs text-muted-foreground">
{translate(
'accounts.managedData.upgrade',
'If accounts do not appear, update or restart the Orca host.'
)}
</p>
)}
{error && (
<p role="alert" className="text-xs text-destructive">
{error}
</p>
)}
{state && (
<>
<div className="flex items-center justify-between gap-3">
<div data-settings-row="" className="flex items-center justify-between gap-3 py-2">
<span className="text-xs">
{translate('accounts.managedData.system', 'System default')}
</span>
@@ -158,7 +160,11 @@ export function ManagedDataAccountsSection({
</Button>
</div>
{state.accounts.map((account) => (
<div key={account.id} className="flex items-center justify-between gap-3">
<div
key={account.id}
data-settings-row=""
className="flex items-center justify-between gap-3 py-2"
>
<div className="space-y-1">
<span className="text-xs">{account.label}</span>
<p className="text-xs text-muted-foreground">{account.integrations.join(', ')}</p>
@@ -232,6 +238,6 @@ export function ManagedDataAccountsSection({
</DialogFooter>
</DialogContent>
</Dialog>
</section>
</SettingsGroup>
)
}
@@ -7,216 +7,217 @@ import { SearchableSetting } from '../../section-layout/SearchableSetting'
import type { AccountsPaneSectionModel } from '../accounts-pane-types'
import { OpenCodeGoCredentials } from '../usage/accounts-pane-opencode-credentials'
import { DebouncedSettingsTextInput } from '../../form-controls/DebouncedSettingsTextInput'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
export function renderGeminiAccountsSection(model: AccountsPaneSectionModel): React.JSX.Element {
const { localAccountRuntimeSentenceLabel, recordFeatureInteraction, settings, updateSettings } =
model
return (
<section key="gemini" id="accounts-gemini" className="space-y-4 scroll-mt-6">
<div className="space-y-1">
<h3 className="flex items-center gap-2 text-sm font-semibold">
<GeminiIcon size={16} />
{translate('auto.components.settings.AccountsPane.0c64dc2a64', 'Gemini CLI (legacy)')}
</h3>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.AccountsPane.973741a871',
'Keep existing Gemini CLI sessions available. Gemini credentials do not sign you into Antigravity.'
)}
</p>
</div>
<SearchableSetting
title={translate(
'auto.components.settings.AccountsPane.0c7f915b01',
'Use Gemini CLI credentials'
)}
<div key="gemini" id="accounts-gemini" className="scroll-mt-6">
<SettingsGroup
title={
<span className="flex items-center gap-2">
<GeminiIcon size={16} />
{translate('auto.components.settings.AccountsPane.0c64dc2a64', 'Gemini CLI (legacy)')}
</span>
}
description={translate(
'auto.components.settings.AccountsPane.d676c41fc6',
'Extracts OAuth credentials from your local Gemini CLI installation to authenticate with Google. This uses credentials issued to the Gemini CLI app, not Orca. May break if Google updates the CLI. Use at your own risk.'
'auto.components.settings.AccountsPane.973741a871',
'Keep existing Gemini CLI sessions available. Gemini credentials do not sign you into Antigravity.'
)}
keywords={[
'gemini',
'cli',
'oauth',
'credentials',
'experimental',
'rate limit',
'status bar'
]}
className="flex items-center justify-between gap-4 py-2"
>
<div className="space-y-0.5">
<Label>
{translate(
<SearchableSetting
title={translate(
'auto.components.settings.AccountsPane.0c7f915b01',
'Use Gemini CLI credentials'
)}
description={translate(
'auto.components.settings.AccountsPane.d676c41fc6',
'Extracts OAuth credentials from your local Gemini CLI installation to authenticate with Google. This uses credentials issued to the Gemini CLI app, not Orca. May break if Google updates the CLI. Use at your own risk.'
)}
keywords={[
'gemini',
'cli',
'oauth',
'credentials',
'experimental',
'rate limit',
'status bar'
]}
className="flex items-center justify-between gap-4 py-3"
>
<div className="space-y-0.5">
<Label>
{translate(
'auto.components.settings.AccountsPane.96f3649526',
'Use Gemini CLI credentials (experimental)'
)}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.AccountsPane.c2aee76420',
'Extracts OAuth credentials from your local Gemini CLI installation to authenticate with Google for {{value0}}. This uses credentials issued to the Gemini CLI app, not Orca. May break if Google updates the CLI. Use at your own risk.',
{ value0: localAccountRuntimeSentenceLabel }
)}
</p>
</div>
<Switch
aria-label={translate(
'auto.components.settings.AccountsPane.96f3649526',
'Use Gemini CLI credentials (experimental)'
)}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.AccountsPane.c2aee76420',
'Extracts OAuth credentials from your local Gemini CLI installation to authenticate with Google for {{value0}}. This uses credentials issued to the Gemini CLI app, not Orca. May break if Google updates the CLI. Use at your own risk.',
{ value0: localAccountRuntimeSentenceLabel }
)}
</p>
</div>
<Switch
aria-label={translate(
'auto.components.settings.AccountsPane.96f3649526',
'Use Gemini CLI credentials (experimental)'
)}
checked={settings.geminiCliOAuthEnabled}
onCheckedChange={(checked) => {
recordFeatureInteraction('usage-tracking')
updateSettings({
geminiCliOAuthEnabled: checked
})
}}
/>
</SearchableSetting>
</section>
checked={settings.geminiCliOAuthEnabled}
onCheckedChange={(checked) => {
recordFeatureInteraction('usage-tracking')
updateSettings({
geminiCliOAuthEnabled: checked
})
}}
/>
</SearchableSetting>
</SettingsGroup>
</div>
)
}
export function renderOpenCodeAccountsSection(model: AccountsPaneSectionModel): React.JSX.Element {
const { recordFeatureInteraction, recordOpenCodeSettingEdit, settings, updateSettings } = model
return (
<section key="opencode-go" id="accounts-opencode-go" className="space-y-4 scroll-mt-6">
<div className="space-y-1">
<h3 className="flex items-center gap-2 text-sm font-semibold">
<OpenCodeGoIcon size={16} />
{translate('auto.components.settings.AccountsPane.4ac10b4d08', 'OpenCode Go')}
</h3>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.AccountsPane.ea631977b5',
'Configure OpenCode Go provider settings.'
)}
</p>
</div>
<OpenCodeGoCredentials onSaved={() => recordOpenCodeSettingEdit('apiKey')} />
<SearchableSetting
title={translate(
'auto.components.settings.AccountsPane.36223200ac',
'OpenCode Go Session Cookie'
)}
<div key="opencode-go" id="accounts-opencode-go" className="scroll-mt-6">
<SettingsGroup
title={
<span className="flex items-center gap-2">
<OpenCodeGoIcon size={16} />
{translate('auto.components.settings.AccountsPane.4ac10b4d08', 'OpenCode Go')}
</span>
}
description={translate(
'auto.components.settings.AccountsPane.0335bd31d5',
'Paste the full opencode.ai Cookie header, including __Host-console_session, for rate limit fetching.'
'auto.components.settings.AccountsPane.ea631977b5',
'Configure OpenCode Go provider settings.'
)}
keywords={['opencode', 'cookie', 'session', 'console', 'rate limit', 'status bar']}
className="space-y-2"
>
<Label>
{translate(
'auto.components.settings.AccountsPane.67e3c33670',
'OpenCode Go session cookie'
)}
</Label>
<div className="flex gap-2">
<DebouncedSettingsTextInput
type="password"
value={settings.opencodeSessionCookie}
onEdit={() => recordOpenCodeSettingEdit('cookie')}
commit={(opencodeSessionCookie) => updateSettings({ opencodeSessionCookie })}
placeholder={translate(
'auto.components.settings.AccountsPane.37b4b4a3f7',
'auth=…; __Host-console_session=…'
)}
spellCheck={false}
className="flex-1 text-xs"
/>
{settings.opencodeSessionCookie && (
<Button
variant="ghost"
size="xs"
onClick={() => {
recordFeatureInteraction('usage-tracking')
updateSettings({ opencodeSessionCookie: '' })
}}
className="h-7 shrink-0 text-xs text-muted-foreground hover:text-foreground"
>
{translate('auto.components.settings.AccountsPane.b398b834c9', 'Clear')}
</Button>
)}
</div>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.AccountsPane.62ab430f94',
"Paste the full Cookie header from your browser's DevTools → Network → any opencode.ai request, including __Host-console_session (e.g."
)}{' '}
<code className="text-xs">
{translate(
'auto.components.settings.AccountsPane.37b4b4a3f7',
'auth=…; __Host-console_session=…'
)}
</code>
{translate(
'auto.components.settings.AccountsPane.d5267cce63',
'). The auth cookie still covers workspace discovery; auth alone is not enough for usage. OpenCode Go auth is web-based and shared across Windows and WSL terminals.'
)}
</p>
</SearchableSetting>
<OpenCodeGoCredentials onSaved={() => recordOpenCodeSettingEdit('apiKey')} />
<SearchableSetting
title={translate(
'auto.components.settings.AccountsPane.02cb127710',
'OpenCode Go Workspace ID'
)}
description={translate(
'auto.components.settings.AccountsPane.d70a5287a4',
'Optional workspace ID override if the automatic lookup fails.'
)}
keywords={['opencode', 'workspace', 'id', 'wrk', 'rate limit', 'status bar']}
className="space-y-2"
>
<Label>
{translate('auto.components.settings.AccountsPane.dbdb0b0bd8', 'Workspace ID override')}
</Label>
<div className="flex gap-2">
<DebouncedSettingsTextInput
type="text"
value={settings.opencodeWorkspaceId}
onEdit={() => recordOpenCodeSettingEdit('workspaceId')}
commit={(opencodeWorkspaceId) => updateSettings({ opencodeWorkspaceId })}
placeholder={translate(
'auto.components.settings.AccountsPane.a122332371',
'wrk_… (leave blank for automatic lookup)'
)}
spellCheck={false}
className="flex-1 text-xs"
/>
{settings.opencodeWorkspaceId && (
<Button
variant="ghost"
size="xs"
onClick={() => {
recordFeatureInteraction('usage-tracking')
updateSettings({ opencodeWorkspaceId: '' })
}}
className="h-7 shrink-0 text-xs text-muted-foreground hover:text-foreground"
>
{translate('auto.components.settings.AccountsPane.b398b834c9', 'Clear')}
</Button>
<SearchableSetting
title={translate(
'auto.components.settings.AccountsPane.36223200ac',
'OpenCode Go Session Cookie'
)}
</div>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.AccountsPane.51c9104e13',
'Find this in the URL after logging into opencode.ai (e.g.'
)}{' '}
<code className="text-xs">
description={translate(
'auto.components.settings.AccountsPane.0335bd31d5',
'Paste the full opencode.ai Cookie header, including __Host-console_session, for rate limit fetching.'
)}
keywords={['opencode', 'cookie', 'session', 'console', 'rate limit', 'status bar']}
className="space-y-2 py-3"
>
<Label>
{translate(
'auto.components.settings.AccountsPane.ae3b21eb6c',
'opencode.ai/workspace/wrk_…/go'
'auto.components.settings.AccountsPane.67e3c33670',
'OpenCode Go session cookie'
)}
</code>
).
</p>
</SearchableSetting>
</section>
</Label>
<div className="flex gap-2">
<DebouncedSettingsTextInput
type="password"
value={settings.opencodeSessionCookie}
onEdit={() => recordOpenCodeSettingEdit('cookie')}
commit={(opencodeSessionCookie) => updateSettings({ opencodeSessionCookie })}
placeholder={translate(
'auto.components.settings.AccountsPane.37b4b4a3f7',
'auth=…; __Host-console_session=…'
)}
spellCheck={false}
className="flex-1 text-xs"
/>
{settings.opencodeSessionCookie && (
<Button
variant="ghost"
size="xs"
onClick={() => {
recordFeatureInteraction('usage-tracking')
updateSettings({ opencodeSessionCookie: '' })
}}
className="h-7 shrink-0 text-xs text-muted-foreground hover:text-foreground"
>
{translate('auto.components.settings.AccountsPane.b398b834c9', 'Clear')}
</Button>
)}
</div>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.AccountsPane.62ab430f94',
"Paste the full Cookie header from your browser's DevTools → Network → any opencode.ai request, including __Host-console_session (e.g."
)}{' '}
<code className="text-xs">
{translate(
'auto.components.settings.AccountsPane.37b4b4a3f7',
'auth=…; __Host-console_session=…'
)}
</code>
{translate(
'auto.components.settings.AccountsPane.d5267cce63',
'). The auth cookie still covers workspace discovery; auth alone is not enough for usage. OpenCode Go auth is web-based and shared across Windows and WSL terminals.'
)}
</p>
</SearchableSetting>
<SearchableSetting
title={translate(
'auto.components.settings.AccountsPane.02cb127710',
'OpenCode Go Workspace ID'
)}
description={translate(
'auto.components.settings.AccountsPane.d70a5287a4',
'Optional workspace ID override if the automatic lookup fails.'
)}
keywords={['opencode', 'workspace', 'id', 'wrk', 'rate limit', 'status bar']}
className="space-y-2 py-3"
>
<Label>
{translate('auto.components.settings.AccountsPane.dbdb0b0bd8', 'Workspace ID override')}
</Label>
<div className="flex gap-2">
<DebouncedSettingsTextInput
type="text"
value={settings.opencodeWorkspaceId}
onEdit={() => recordOpenCodeSettingEdit('workspaceId')}
commit={(opencodeWorkspaceId) => updateSettings({ opencodeWorkspaceId })}
placeholder={translate(
'auto.components.settings.AccountsPane.a122332371',
'wrk_… (leave blank for automatic lookup)'
)}
spellCheck={false}
className="flex-1 text-xs"
/>
{settings.opencodeWorkspaceId && (
<Button
variant="ghost"
size="xs"
onClick={() => {
recordFeatureInteraction('usage-tracking')
updateSettings({ opencodeWorkspaceId: '' })
}}
className="h-7 shrink-0 text-xs text-muted-foreground hover:text-foreground"
>
{translate('auto.components.settings.AccountsPane.b398b834c9', 'Clear')}
</Button>
)}
</div>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.AccountsPane.51c9104e13',
'Find this in the URL after logging into opencode.ai (e.g.'
)}{' '}
<code className="text-xs">
{translate(
'auto.components.settings.AccountsPane.ae3b21eb6c',
'opencode.ai/workspace/wrk_…/go'
)}
</code>
).
</p>
</SearchableSetting>
</SettingsGroup>
</div>
)
}
@@ -7,6 +7,7 @@ import { getHostRuntimeLabel } from './accounts-pane-runtime'
import { WSL_DEFAULT_DISTRO_KEY } from '../../../provider-accounts/provider-account-visibility'
import type { GlobalSettings } from '../../../../../../shared/global-settings-types'
import type { AccountsPaneSectionModel } from '../accounts-pane-types'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
export function renderAccountsLocationSection(
model: AccountsPaneSectionModel
@@ -126,8 +127,8 @@ export function renderAccountsLocationSection(
) : null
return (
<section key="account-runtime" id="accounts-runtime" className="space-y-3 scroll-mt-6">
{accountRuntimeControls}
</section>
<div key="account-runtime" id="accounts-runtime" className="scroll-mt-6">
<SettingsGroup>{accountRuntimeControls}</SettingsGroup>
</div>
)
}
@@ -16,6 +16,7 @@ import {
type UsageCredentialHost
} from './usage-credential-host'
import type { UsageHostSettingsModel } from './use-usage-host-settings'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
export function MiniMaxAccountsSection({
host,
@@ -47,176 +48,177 @@ export function MiniMaxAccountsSection({
void usageSettings.update({ minimaxEndpoint: value })
}
return (
<section id="accounts-minimax" className="space-y-4 scroll-mt-6">
<div className="flex items-start justify-between gap-3">
<div className="space-y-1">
<h3 className="flex items-center gap-2 text-sm font-semibold">
<div id="accounts-minimax" className="scroll-mt-6">
<SettingsGroup
title={
<span className="flex items-center gap-2">
<MiniMaxIcon size={16} />
{translate('auto.components.settings.AccountsPane.5d63bbfbec', 'MiniMax')}
</h3>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.AccountsPane.usageTracking',
'Configure MiniMax usage tracking for your account.'
)}
</p>
</div>
<a
href={consoleUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
>
{translate('auto.components.settings.AccountsPane.0d8e77bc40', 'Open console')}
<ExternalLink className="size-3" />
</a>
</div>
<div
className={cn(
'flex items-start gap-3 rounded-lg border bg-muted/20 p-3',
configured ? 'border-border/60' : 'border-border/40'
)}
>
<ShieldCheck
className={cn(
'mt-0.5 size-4 shrink-0',
configured ? 'text-foreground' : 'text-muted-foreground'
)}
/>
<div className="space-y-0.5">
{accessNotice ? (
<p className="text-xs font-medium">{accessNotice}</p>
) : (
<p className="text-xs font-medium">
{configured
? usageCredentialStoredLabel(host)
: translate(
'auto.components.settings.AccountsPane.credentialsNotSet',
'Credentials not set'
)}
</p>
)}
<p className="text-xs text-muted-foreground">{miniMaxStorageHelp(host)}</p>
</div>
</div>
<SearchableSetting
title={translate('auto.components.settings.AccountsPane.f8a4b9d210', 'MiniMax endpoint')}
</span>
}
description={translate(
'auto.components.settings.AccountsPane.0b3a9f6c2e',
'Pick the host that matches your account. Both overseas (platform.minimax.io) and China (platform.minimaxi.com) accept either a session cookie or an API key.'
'auto.components.settings.AccountsPane.usageTracking',
'Configure MiniMax usage tracking for your account.'
)}
keywords={['minimax', 'endpoint', 'region', 'host', 'cn', 'china', 'overseas', 'api key']}
className="space-y-2"
action={
<a
href={consoleUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
>
{translate('auto.components.settings.AccountsPane.0d8e77bc40', 'Open console')}
<ExternalLink className="size-3" />
</a>
}
>
<Label htmlFor="minimax-endpoint">
{translate('auto.components.settings.AccountsPane.f8a4b9d210', 'MiniMax endpoint')}
</Label>
<Select
value={settings?.minimaxEndpoint ?? ''}
onValueChange={handleMiniMaxEndpointChange}
disabled={miniMaxCredentialBusy || settingsUnavailable}
>
<SelectTrigger id="minimax-endpoint" className="h-8 w-full text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{[
{
value: 'overseas',
label: translate(
'auto.components.settings.AccountsPane.endpointOverseas',
'Overseas (platform.minimax.io)'
)
},
{
value: 'cn',
label: translate(
'auto.components.settings.AccountsPane.endpointChina',
'China (platform.minimaxi.com)'
)
}
].map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</SearchableSetting>
<MiniMaxCredentials
credentials={credentials}
host={host}
miniMaxRateLimits={miniMaxRateLimits}
consoleUrl={consoleUrl}
/>
<div className="space-y-3 rounded-lg border border-border/60 bg-muted/20 p-3">
<div className="flex items-center justify-between gap-3">
<div className="space-y-1">
<h4 className="text-xs font-semibold text-muted-foreground">
{translate('auto.components.settings.AccountsPane.9dd50d3f75', 'Advanced')}
</h4>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.AccountsPane.174fb408f9',
'Leave these defaults alone unless MiniMax usage refresh points at the wrong workspace or model.'
)}
</p>
<div className="flex items-start gap-3 py-3">
<ShieldCheck
className={cn(
'mt-0.5 size-4 shrink-0',
configured ? 'text-foreground' : 'text-muted-foreground'
)}
/>
<div className="space-y-0.5">
{accessNotice ? (
<p className="text-xs font-medium">{accessNotice}</p>
) : (
<p className="text-xs font-medium">
{configured
? usageCredentialStoredLabel(host)
: translate(
'auto.components.settings.AccountsPane.credentialsNotSet',
'Credentials not set'
)}
</p>
)}
<p className="text-xs text-muted-foreground">{miniMaxStorageHelp(host)}</p>
</div>
</div>
<SearchableSetting
title={translate('auto.components.settings.AccountsPane.bf160bb6c0', 'Group ID override')}
title={translate('auto.components.settings.AccountsPane.f8a4b9d210', 'MiniMax endpoint')}
description={translate(
'auto.components.settings.AccountsPane.b1e2743313',
'Optional. Leave blank to use minimax_group_id_v2 from the cookie.'
'auto.components.settings.AccountsPane.0b3a9f6c2e',
'Pick the host that matches your account. Both overseas (platform.minimax.io) and China (platform.minimaxi.com) accept either a session cookie or an API key.'
)}
keywords={['minimax', 'group', 'id', 'rate limit']}
className="space-y-2"
keywords={['minimax', 'endpoint', 'region', 'host', 'cn', 'china', 'overseas', 'api key']}
className="space-y-2 py-3"
>
<Label>
{translate('auto.components.settings.AccountsPane.bf160bb6c0', 'Group ID override')}
<Label htmlFor="minimax-endpoint">
{translate('auto.components.settings.AccountsPane.f8a4b9d210', 'MiniMax endpoint')}
</Label>
<DebouncedSettingsTextInput
type="text"
value={settings?.minimaxGroupId ?? ''}
commit={(minimaxGroupId) => void usageSettings.update({ minimaxGroupId })}
disabled={settingsUnavailable}
placeholder={translate(
'auto.components.settings.AccountsPane.0747d6391a',
'Use group ID from cookie'
)}
spellCheck={false}
className="text-xs"
/>
<Select
value={settings?.minimaxEndpoint ?? ''}
onValueChange={handleMiniMaxEndpointChange}
disabled={miniMaxCredentialBusy || settingsUnavailable}
>
<SelectTrigger id="minimax-endpoint" className="h-8 w-full text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{[
{
value: 'overseas',
label: translate(
'auto.components.settings.AccountsPane.endpointOverseas',
'Overseas (platform.minimax.io)'
)
},
{
value: 'cn',
label: translate(
'auto.components.settings.AccountsPane.endpointChina',
'China (platform.minimaxi.com)'
)
}
].map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</SearchableSetting>
<SearchableSetting
title={translate('auto.components.settings.AccountsPane.4ff2af7524', 'Usage model names')}
description={translate(
'auto.components.settings.AccountsPane.5cf4b0f85f',
'Optional comma-separated model names. Leave as general unless MiniMax returns a model-specific error.'
)}
keywords={['minimax', 'model', 'general', 'rate limit']}
className="space-y-2"
>
<Label>
{translate('auto.components.settings.AccountsPane.4ff2af7524', 'Usage model names')}
</Label>
<DebouncedSettingsTextInput
type="text"
value={settings?.minimaxUsageModels ?? ''}
commit={(minimaxUsageModels) => void usageSettings.update({ minimaxUsageModels })}
disabled={settingsUnavailable}
placeholder={translate('auto.components.settings.AccountsPane.3c92b0d31c', 'general')}
spellCheck={false}
className="text-xs"
/>
</SearchableSetting>
</div>
</section>
<MiniMaxCredentials
credentials={credentials}
host={host}
miniMaxRateLimits={miniMaxRateLimits}
consoleUrl={consoleUrl}
/>
<div>
<div className="flex items-center justify-between gap-3 pt-4 pb-3">
<div className="space-y-1">
<h4 className="text-xs font-semibold text-muted-foreground">
{translate('auto.components.settings.AccountsPane.9dd50d3f75', 'Advanced')}
</h4>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.AccountsPane.174fb408f9',
'Leave these defaults alone unless MiniMax usage refresh points at the wrong workspace or model.'
)}
</p>
</div>
</div>
<SearchableSetting
title={translate(
'auto.components.settings.AccountsPane.bf160bb6c0',
'Group ID override'
)}
description={translate(
'auto.components.settings.AccountsPane.b1e2743313',
'Optional. Leave blank to use minimax_group_id_v2 from the cookie.'
)}
keywords={['minimax', 'group', 'id', 'rate limit']}
className="space-y-2 py-3"
>
<Label>
{translate('auto.components.settings.AccountsPane.bf160bb6c0', 'Group ID override')}
</Label>
<DebouncedSettingsTextInput
type="text"
value={settings?.minimaxGroupId ?? ''}
commit={(minimaxGroupId) => void usageSettings.update({ minimaxGroupId })}
disabled={settingsUnavailable}
placeholder={translate(
'auto.components.settings.AccountsPane.0747d6391a',
'Use group ID from cookie'
)}
spellCheck={false}
className="text-xs"
/>
</SearchableSetting>
<SearchableSetting
title={translate(
'auto.components.settings.AccountsPane.4ff2af7524',
'Usage model names'
)}
description={translate(
'auto.components.settings.AccountsPane.5cf4b0f85f',
'Optional comma-separated model names. Leave as general unless MiniMax returns a model-specific error.'
)}
keywords={['minimax', 'model', 'general', 'rate limit']}
className="space-y-2 py-3"
>
<Label>
{translate('auto.components.settings.AccountsPane.4ff2af7524', 'Usage model names')}
</Label>
<DebouncedSettingsTextInput
type="text"
value={settings?.minimaxUsageModels ?? ''}
commit={(minimaxUsageModels) => void usageSettings.update({ minimaxUsageModels })}
disabled={settingsUnavailable}
placeholder={translate('auto.components.settings.AccountsPane.3c92b0d31c', 'general')}
spellCheck={false}
className="text-xs"
/>
</SearchableSetting>
</div>
</SettingsGroup>
</div>
)
}
@@ -28,6 +28,7 @@ import {
type UsageCredentialHost
} from './usage-credential-host'
import type { UsageHostSettingsModel } from './use-usage-host-settings'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
const SEARCH_KEYWORDS = [
'glm',
@@ -103,308 +104,304 @@ export function ZcodePlanAccountsSection({
const staleUsageError = usage?.status === 'error' ? (usage.error ?? null) : null
return (
<section id="accounts-zcode" className="space-y-4 scroll-mt-6">
<div className="flex items-start justify-between gap-3">
<div className="space-y-1">
<h3 className="flex items-center gap-2 text-sm font-semibold">
<div id="accounts-zcode" className="scroll-mt-6">
<SettingsGroup
title={
<span className="flex items-center gap-2">
<AgentIcon agent="zcode" size={16} />
{translate(
'auto.components.settings.ZcodePlanAccountsSection.title',
'GLM Coding Plan'
)}
</h3>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.ZcodePlanAccountsSection.subtitle',
'Track Z.AI or Zhipu (BigModel) GLM Coding Plan usage in the status bar. Save the plan API key here — no ZCode CLI setup needed.'
)}
</p>
</div>
{consoleUrl ? (
<a
href={consoleUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
>
{translate(
'auto.components.settings.ZcodePlanAccountsSection.consoleLink',
'Get API key'
)}
<ExternalLink className="size-3" />
</a>
) : null}
</div>
<div
className={cn(
'flex items-start gap-3 rounded-lg border bg-muted/20 p-3',
apiKeyConfigured ? 'border-border/60' : 'border-border/40'
)}
>
<ShieldCheck
className={cn(
'mt-0.5 size-4 shrink-0',
apiKeyConfigured ? 'text-foreground' : 'text-muted-foreground'
)}
/>
<div className="min-w-0 flex-1 space-y-0.5">
{accessNotice ? (
<p className="text-xs font-medium">{accessNotice}</p>
) : detailsUnavailable ? (
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.ZcodePlanAccountsSection.detailsUnavailable',
'Plan credential details are only readable on the computer running Orca.'
)}
</p>
) : apiKeyConfigured ? (
<>
<p className="text-xs font-medium">
{translate(
'auto.components.settings.ZcodePlanAccountsSection.keyStored',
'API key saved · {{value0}}',
{ value0: siteLabel(site ?? 'zai') }
)}
</p>
<p className="text-xs text-muted-foreground">
{hostLabel === null
? translate(
'auto.components.settings.ZcodePlanAccountsSection.keyStoredHelp',
'Stored locally and sent only to the selected site for usage refreshes. It takes priority over the ZCode CLI sign-in.'
)
: translate(
'auto.components.settings.ZcodePlanAccountsSection.keyStoredOnHostHelp',
'Stored on {{host}} and sent from there only to the selected site for usage refreshes. It takes priority over the ZCode CLI sign-in.',
{ host: hostLabel }
)}
</p>
</>
) : status?.zcodeCliConfigured ? (
<>
<p className="text-xs font-medium">
{translate(
'auto.components.settings.ZcodePlanAccountsSection.usingCli',
'Using the ZCode CLI sign-in'
)}
</p>
<p className="text-xs text-muted-foreground">
{hostLabel === null
? translate(
'auto.components.settings.ZcodePlanAccountsSection.usingCliHelp',
'Orca reads the selected Coding Plan from the ZCode CLI credential store on this computer. Save an API key below to link the plan here instead.'
)
: translate(
'auto.components.settings.ZcodePlanAccountsSection.usingCliOnHostHelp',
'Orca reads the selected Coding Plan from the ZCode CLI credential store on {{host}}. Save an API key below to link the plan there instead.',
{ host: hostLabel }
)}
</p>
</>
) : (
<>
<p className="text-xs font-medium">
{translate(
'auto.components.settings.ZcodePlanAccountsSection.notConfigured',
'No GLM Coding Plan linked'
)}
</p>
<p className="text-xs text-muted-foreground">
{hostLabel === null
? translate(
'auto.components.settings.ZcodePlanAccountsSection.notConfiguredHelp',
'Save the plan API key below, or sign in with the ZCode CLI on this computer.'
)
: translate(
'auto.components.settings.ZcodePlanAccountsSection.notConfiguredOnHostHelp',
'Save the plan API key below, or sign in with the ZCode CLI on {{host}}.',
{ host: hostLabel }
)}
</p>
</>
)}
{staleUsageError ? <p className="text-xs text-destructive">{staleUsageError}</p> : null}
</div>
{hostLabel === null ? (
<Button
variant="outline"
size="xs"
disabled={refreshing}
onClick={() => void handleRefreshUsage()}
className="shrink-0"
>
{refreshing ? (
<Loader2 className="size-3 animate-spin" />
) : (
<RefreshCw className="size-3" />
)}
{translate(
'auto.components.settings.ZcodePlanAccountsSection.refreshUsage',
'Refresh usage'
)}
</Button>
) : null}
</div>
{webClient ? (
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.ZcodePlanAccountsSection.hostOnly',
'Change the plan site and API key in the desktop app on the computer running Orca.'
)}
</p>
) : null}
<SearchableSetting
title={translate(
'auto.components.settings.ZcodePlanAccountsSection.siteTitle',
'Plan site'
)}
</span>
}
description={translate(
'auto.components.settings.ZcodePlanAccountsSection.siteDescription',
'Pick the console your Coding Plan belongs to: Z.AI for the international site, Zhipu BigModel for the mainland site.'
'auto.components.settings.ZcodePlanAccountsSection.subtitle',
'Track Z.AI or Zhipu (BigModel) GLM Coding Plan usage in the status bar. Save the plan API key here — no ZCode CLI setup needed.'
)}
keywords={SEARCH_KEYWORDS}
className="space-y-2"
>
<Label htmlFor="zcode-plan-site">
{translate('auto.components.settings.ZcodePlanAccountsSection.siteTitle', 'Plan site')}
</Label>
<Select
value={site ?? ''}
onValueChange={handleSiteChange}
disabled={credentialBusy || !siteEditable}
>
<SelectTrigger id="zcode-plan-site" size="sm" className="w-full">
<SelectValue
placeholder={translate(
'auto.components.settings.ZcodePlanAccountsSection.siteUnavailable',
'Host plan site unavailable'
)}
/>
</SelectTrigger>
<SelectContent>
{(['zai', 'bigmodel'] as const).map((option) => (
<SelectItem key={option} value={option}>
{siteLabel(option)}
</SelectItem>
))}
</SelectContent>
</Select>
</SearchableSetting>
<SearchableSetting
title={translate('auto.components.settings.ZcodePlanAccountsSection.keyTitle', 'API key')}
description={zcodePlanKeyDescription(host)}
keywords={SEARCH_KEYWORDS}
className="space-y-2"
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<Label htmlFor="zcode-plan-api-key">
{translate('auto.components.settings.ZcodePlanAccountsSection.keyTitle', 'API key')}
</Label>
{!detailsUnavailable ? (
<Badge variant={apiKeyConfigured ? 'secondary' : 'outline'}>
{apiKeyConfigured ? <Lock className="size-3" /> : <LockOpen className="size-3" />}
{apiKeyConfigured
? translate('auto.components.settings.ZcodePlanAccountsSection.saved', 'Saved')
: translate(
'auto.components.settings.ZcodePlanAccountsSection.notSaved',
'Not saved'
)}
</Badge>
) : null}
</div>
</div>
<UnsealedCredentialNotice
protection={status?.apiKeyProtection ?? null}
credentialName={translate(
'auto.components.settings.ZcodePlanAccountsSection.title',
'GLM Coding Plan'
)}
/>
{unreadableNotice ? (
<p className="text-xs text-muted-foreground">{unreadableNotice}</p>
) : null}
<div className="flex gap-2">
<Input
id="zcode-plan-api-key"
type="password"
disabled={credentialBusy || !keyEditable}
value={apiKeyDraft}
onChange={(e) => setApiKeyDraft(e.target.value)}
placeholder={translate(
'auto.components.settings.ZcodePlanAccountsSection.keyPlaceholder',
'Paste your GLM Coding Plan API key'
)}
spellCheck={false}
className="flex-1"
/>
<Button
size="xs"
onClick={() => void saveApiKey()}
disabled={credentialBusy || !keyEditable || !apiKeyDraft.trim()}
className="shrink-0"
>
{credentialBusy ? <Loader2 className="size-3 animate-spin" /> : null}
{apiKeyConfigured
? translate('auto.components.settings.ZcodePlanAccountsSection.replace', 'Replace')
: translate('auto.components.settings.ZcodePlanAccountsSection.save', 'Save')}
</Button>
{apiKeyConfigured ? (
<Button
variant="ghost"
size="xs"
onClick={() => void clearApiKey()}
disabled={credentialBusy || !credentialEditable}
className="shrink-0"
action={
consoleUrl ? (
<a
href={consoleUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
>
{translate(
'auto.components.settings.ZcodePlanAccountsSection.forgetKey',
'Forget key'
'auto.components.settings.ZcodePlanAccountsSection.consoleLink',
'Get API key'
)}
<ExternalLink className="size-3" />
</a>
) : null
}
>
<div className="flex items-start gap-3 py-3">
<ShieldCheck
className={cn(
'mt-0.5 size-4 shrink-0',
apiKeyConfigured ? 'text-foreground' : 'text-muted-foreground'
)}
/>
<div className="min-w-0 flex-1 space-y-0.5">
{accessNotice ? (
<p className="text-xs font-medium">{accessNotice}</p>
) : detailsUnavailable ? (
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.ZcodePlanAccountsSection.detailsUnavailable',
'Plan credential details are only readable on the computer running Orca.'
)}
</p>
) : apiKeyConfigured ? (
<>
<p className="text-xs font-medium">
{translate(
'auto.components.settings.ZcodePlanAccountsSection.keyStored',
'API key saved · {{value0}}',
{ value0: siteLabel(site ?? 'zai') }
)}
</p>
<p className="text-xs text-muted-foreground">
{hostLabel === null
? translate(
'auto.components.settings.ZcodePlanAccountsSection.keyStoredHelp',
'Stored locally and sent only to the selected site for usage refreshes. It takes priority over the ZCode CLI sign-in.'
)
: translate(
'auto.components.settings.ZcodePlanAccountsSection.keyStoredOnHostHelp',
'Stored on {{host}} and sent from there only to the selected site for usage refreshes. It takes priority over the ZCode CLI sign-in.',
{ host: hostLabel }
)}
</p>
</>
) : status?.zcodeCliConfigured ? (
<>
<p className="text-xs font-medium">
{translate(
'auto.components.settings.ZcodePlanAccountsSection.usingCli',
'Using the ZCode CLI sign-in'
)}
</p>
<p className="text-xs text-muted-foreground">
{hostLabel === null
? translate(
'auto.components.settings.ZcodePlanAccountsSection.usingCliHelp',
'Orca reads the selected Coding Plan from the ZCode CLI credential store on this computer. Save an API key below to link the plan here instead.'
)
: translate(
'auto.components.settings.ZcodePlanAccountsSection.usingCliOnHostHelp',
'Orca reads the selected Coding Plan from the ZCode CLI credential store on {{host}}. Save an API key below to link the plan there instead.',
{ host: hostLabel }
)}
</p>
</>
) : (
<>
<p className="text-xs font-medium">
{translate(
'auto.components.settings.ZcodePlanAccountsSection.notConfigured',
'No GLM Coding Plan linked'
)}
</p>
<p className="text-xs text-muted-foreground">
{hostLabel === null
? translate(
'auto.components.settings.ZcodePlanAccountsSection.notConfiguredHelp',
'Save the plan API key below, or sign in with the ZCode CLI on this computer.'
)
: translate(
'auto.components.settings.ZcodePlanAccountsSection.notConfiguredOnHostHelp',
'Save the plan API key below, or sign in with the ZCode CLI on {{host}}.',
{ host: hostLabel }
)}
</p>
</>
)}
{staleUsageError ? <p className="text-xs text-destructive">{staleUsageError}</p> : null}
</div>
{hostLabel === null ? (
<Button
variant="outline"
size="xs"
disabled={refreshing}
onClick={() => void handleRefreshUsage()}
className="shrink-0"
>
{refreshing ? (
<Loader2 className="size-3 animate-spin" />
) : (
<RefreshCw className="size-3" />
)}
{translate(
'auto.components.settings.ZcodePlanAccountsSection.refreshUsage',
'Refresh usage'
)}
</Button>
) : null}
</div>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.ZcodePlanAccountsSection.keyHelp',
'The same key your coding tools use for the plan (for example Claude Code with ANTHROPIC_BASE_URL pointed at the site). Switching the site above changes which host receives it.'
)}
</p>
</SearchableSetting>
{!webClient && hostLabel === null ? <ZcodeCliSetupSection /> : null}
{webClient ? (
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.ZcodePlanAccountsSection.hostOnly',
'Change the plan site and API key in the desktop app on the computer running Orca.'
)}
</p>
) : null}
{usageWindows.length > 0 ? (
<SearchableSetting
title={translate(
'auto.components.settings.ZcodePlanAccountsSection.usageTitle',
'Plan usage'
'auto.components.settings.ZcodePlanAccountsSection.siteTitle',
'Plan site'
)}
description={translate(
'auto.components.settings.ZcodePlanAccountsSection.usageDescription',
'Live quota windows for the linked Coding Plan, refreshed with the status bar usage cycle.'
'auto.components.settings.ZcodePlanAccountsSection.siteDescription',
'Pick the console your Coding Plan belongs to: Z.AI for the international site, Zhipu BigModel for the mainland site.'
)}
keywords={SEARCH_KEYWORDS}
className="space-y-2 py-3"
>
<div className="space-y-1">
{usageWindows.map((row) => (
<ZcodeUsageWindowView key={row.kind} row={row} now={now} />
))}
{usage?.planType ? (
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.ZcodePlanAccountsSection.planLevel',
'Plan: {{value0}}',
{ value0: usage.planType }
<Label htmlFor="zcode-plan-site">
{translate('auto.components.settings.ZcodePlanAccountsSection.siteTitle', 'Plan site')}
</Label>
<Select
value={site ?? ''}
onValueChange={handleSiteChange}
disabled={credentialBusy || !siteEditable}
>
<SelectTrigger id="zcode-plan-site" size="sm" className="w-full">
<SelectValue
placeholder={translate(
'auto.components.settings.ZcodePlanAccountsSection.siteUnavailable',
'Host plan site unavailable'
)}
</p>
/>
</SelectTrigger>
<SelectContent>
{(['zai', 'bigmodel'] as const).map((option) => (
<SelectItem key={option} value={option}>
{siteLabel(option)}
</SelectItem>
))}
</SelectContent>
</Select>
</SearchableSetting>
<SearchableSetting
title={translate('auto.components.settings.ZcodePlanAccountsSection.keyTitle', 'API key')}
description={zcodePlanKeyDescription(host)}
keywords={SEARCH_KEYWORDS}
className="space-y-2 py-3"
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<Label htmlFor="zcode-plan-api-key">
{translate('auto.components.settings.ZcodePlanAccountsSection.keyTitle', 'API key')}
</Label>
{!detailsUnavailable ? (
<Badge variant={apiKeyConfigured ? 'secondary' : 'outline'}>
{apiKeyConfigured ? <Lock className="size-3" /> : <LockOpen className="size-3" />}
{apiKeyConfigured
? translate('auto.components.settings.ZcodePlanAccountsSection.saved', 'Saved')
: translate(
'auto.components.settings.ZcodePlanAccountsSection.notSaved',
'Not saved'
)}
</Badge>
) : null}
</div>
</div>
<UnsealedCredentialNotice
protection={status?.apiKeyProtection ?? null}
credentialName={translate(
'auto.components.settings.ZcodePlanAccountsSection.title',
'GLM Coding Plan'
)}
/>
{unreadableNotice ? (
<p className="text-xs text-muted-foreground">{unreadableNotice}</p>
) : null}
<div className="flex gap-2">
<Input
id="zcode-plan-api-key"
type="password"
disabled={credentialBusy || !keyEditable}
value={apiKeyDraft}
onChange={(e) => setApiKeyDraft(e.target.value)}
placeholder={translate(
'auto.components.settings.ZcodePlanAccountsSection.keyPlaceholder',
'Paste your GLM Coding Plan API key'
)}
spellCheck={false}
className="flex-1"
/>
<Button
size="xs"
onClick={() => void saveApiKey()}
disabled={credentialBusy || !keyEditable || !apiKeyDraft.trim()}
className="shrink-0"
>
{credentialBusy ? <Loader2 className="size-3 animate-spin" /> : null}
{apiKeyConfigured
? translate('auto.components.settings.ZcodePlanAccountsSection.replace', 'Replace')
: translate('auto.components.settings.ZcodePlanAccountsSection.save', 'Save')}
</Button>
{apiKeyConfigured ? (
<Button
variant="ghost"
size="xs"
onClick={() => void clearApiKey()}
disabled={credentialBusy || !credentialEditable}
className="shrink-0"
>
{translate(
'auto.components.settings.ZcodePlanAccountsSection.forgetKey',
'Forget key'
)}
</Button>
) : null}
</div>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.ZcodePlanAccountsSection.keyHelp',
'The same key your coding tools use for the plan (for example Claude Code with ANTHROPIC_BASE_URL pointed at the site). Switching the site above changes which host receives it.'
)}
</p>
</SearchableSetting>
) : null}
</section>
{!webClient && hostLabel === null ? <ZcodeCliSetupSection /> : null}
{usageWindows.length > 0 ? (
<SearchableSetting
title={translate(
'auto.components.settings.ZcodePlanAccountsSection.usageTitle',
'Plan usage'
)}
description={translate(
'auto.components.settings.ZcodePlanAccountsSection.usageDescription',
'Live quota windows for the linked Coding Plan, refreshed with the status bar usage cycle.'
)}
keywords={SEARCH_KEYWORDS}
className="py-3"
>
<div className="space-y-1">
{usageWindows.map((row) => (
<ZcodeUsageWindowView key={row.kind} row={row} now={now} />
))}
{usage?.planType ? (
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.ZcodePlanAccountsSection.planLevel',
'Plan: {{value0}}',
{ value0: usage.planType }
)}
</p>
) : null}
</div>
</SearchableSetting>
) : null}
</SettingsGroup>
</div>
)
}
@@ -145,7 +145,7 @@ export function MiniMaxCredentials({
'Paste your MiniMax session cookie for local rate-limit fetching.'
)}
keywords={['minimax', 'cookie', 'session', 'rate limit', 'status bar']}
className="space-y-2"
className="space-y-2 py-3"
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
@@ -277,7 +277,7 @@ export function MiniMaxCredentials({
)
}
keywords={['minimax', 'api', 'key', 'cn', 'china', 'bearer', 'rate limit']}
className="space-y-2"
className="space-y-2 py-3"
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
@@ -65,7 +65,7 @@ export function OpenCodeGoCredentials({ onSaved }: { onSaved: () => void }): Rea
'Optional override. Orca otherwise uses the key OpenCode saved when you ran /connect, then OPENCODE_API_KEY.'
)}
keywords={['opencode', 'go', 'api', 'key', 'connect', 'rate limit', 'status bar']}
className="space-y-2"
className="space-y-2 py-3"
>
<div className="flex items-center gap-2">
<Label htmlFor="opencode-go-api-key">
@@ -7,9 +7,10 @@ import { Label } from '../../ui/label'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../ui/tooltip'
import { AdvancedNetworkSettingsSection } from './network/AdvancedNetworkSettingsSection'
import { SearchableSetting } from '../section-layout/SearchableSetting'
import { SettingsSubsectionHeader, SettingsSwitch } from '../form-controls/SettingsFormControls'
import { SettingsSwitch } from '../form-controls/SettingsFormControls'
import { getAdvancedSearchEntry } from './advanced-search'
import { translate } from '@/i18n/i18n'
import { SettingsGroup } from '../section-layout/SettingsGroup'
type AdvancedPaneProps = {
settings: GlobalSettings
@@ -39,16 +40,14 @@ export function AdvancedPane({ settings, updateSettings }: AdvancedPaneProps): R
}
return (
<div className="space-y-4">
<section className="space-y-3">
<SettingsSubsectionHeader
title={translate('auto.components.settings.AdvancedPane.8d8d8ac599', 'Compatibility')}
description={translate(
'auto.components.settings.AdvancedPane.8b7a8df299',
'Low-level workarounds for support troubleshooting.'
)}
/>
<div className="space-y-8">
<SettingsGroup
title={translate('auto.components.settings.AdvancedPane.8d8d8ac599', 'Compatibility')}
description={translate(
'auto.components.settings.AdvancedPane.8b7a8df299',
'Low-level workarounds for support troubleshooting.'
)}
>
<SearchableSetting
title={getAdvancedSearchEntry().http1Compatibility.title}
description={getAdvancedSearchEntry().http1Compatibility.description}
@@ -133,18 +132,17 @@ export function AdvancedPane({ settings, updateSettings }: AdvancedPaneProps): R
</div>
) : null}
</SearchableSetting>
</section>
</SettingsGroup>
<section className="space-y-3">
<SettingsSubsectionHeader
title={translate('auto.components.settings.AdvancedPane.network', 'Network')}
description={translate(
'auto.components.settings.AdvancedPane.networkDescription',
'App-level network routing for proxies and corporate environments.'
)}
/>
<SettingsGroup
title={translate('auto.components.settings.AdvancedPane.network', 'Network')}
description={translate(
'auto.components.settings.AdvancedPane.networkDescription',
'App-level network routing for proxies and corporate environments.'
)}
>
<AdvancedNetworkSettingsSection settings={settings} updateSettings={updateSettings} />
</section>
</SettingsGroup>
</div>
)
}
@@ -2,7 +2,7 @@ import { toast } from 'sonner'
import { Bell, Info, OctagonX, Sparkles, Trash2 } from 'lucide-react'
import { Button } from '../../ui/button'
import { Badge } from '../../ui/badge'
import { SettingsSubsectionHeader } from '../form-controls/SettingsFormControls'
import { SettingsGroup } from '../section-layout/SettingsGroup'
import { showDeleteWorktreeFailureToast } from '../../sidebar/workspace-actions/deletion/delete-worktree-failure-toast'
import { showLocalBaseRefUpdateSuggestionToast } from '../../sidebar/workspace-creation/base-ref-update/local-base-ref-suggestion-toast'
import { useOrcaProfileAuthStatusRefresh } from '@/hooks/use-orca-profile-auth-status-refresh'
@@ -145,20 +145,19 @@ function OrcaCloudDevSubsection(): React.JSX.Element {
useOrcaProfileAuthStatusRefresh()
return (
<section className="space-y-3">
<div className="flex items-start justify-between gap-3">
<SettingsSubsectionHeader
title={translate('auto.components.settings.DevToolsPane.orcaCloud', 'Orca Cloud')}
description={translate(
'auto.components.settings.DevToolsPane.orcaCloudDescription',
'Dev-only preview of first-party cloud sign-in. Hidden in production; in dev it also appears in the sidebar account switcher once ORCA_CLOUD_API_URL and ORCA_CLOUD_CLIENT_ID are set.'
)}
/>
<Badge variant="outline" className="mt-0.5">
<SettingsGroup
title={translate('auto.components.settings.DevToolsPane.orcaCloud', 'Orca Cloud')}
description={translate(
'auto.components.settings.DevToolsPane.orcaCloudDescription',
'Dev-only preview of first-party cloud sign-in. Hidden in production; in dev it also appears in the sidebar account switcher once ORCA_CLOUD_API_URL and ORCA_CLOUD_CLIENT_ID are set.'
)}
action={
<Badge variant="outline">
{translate('auto.components.settings.DevToolsPane.devOnly', 'Dev only')}
</Badge>
</div>
}
bodyClassName="py-4"
>
{configured ? (
<div className="space-y-2">
<p className="text-xs text-muted-foreground">
@@ -196,7 +195,7 @@ function OrcaCloudDevSubsection(): React.JSX.Element {
)}
</p>
)}
</section>
</SettingsGroup>
)
}
@@ -298,29 +297,28 @@ export function DevToolsPane(): React.JSX.Element {
return (
<div className="space-y-8">
<section className="space-y-3">
<div className="flex items-start justify-between gap-3">
<SettingsSubsectionHeader
title={translate(
'auto.components.settings.DevToolsPane.notificationPlayground',
'Notification playground'
)}
description={translate(
'auto.components.settings.DevToolsPane.notificationPlaygroundDescription',
'Dev-only triggers for checking toast layout, recovery actions, and long-copy wrapping.'
)}
/>
<Badge variant="outline" className="mt-0.5">
<SettingsGroup
title={translate(
'auto.components.settings.DevToolsPane.notificationPlayground',
'Notification playground'
)}
description={translate(
'auto.components.settings.DevToolsPane.notificationPlaygroundDescription',
'Dev-only triggers for checking toast layout, recovery actions, and long-copy wrapping.'
)}
action={
<Badge variant="outline">
{translate('auto.components.settings.DevToolsPane.devOnly', 'Dev only')}
</Badge>
</div>
}
bodyClassName="py-4"
>
<div className="grid gap-2 sm:grid-cols-2">
{actions.map((action) => (
<DevToastActionButton key={action.title} action={action} />
))}
</div>
</section>
</SettingsGroup>
<OrcaCloudDevSubsection />
</div>
@@ -3,6 +3,7 @@ import { Label } from '../../ui/label'
import { Switch } from '../../ui/switch'
import { useAppStore } from '../../../store/index'
import { SearchableSetting } from '../section-layout/SearchableSetting'
import { SettingsGroup } from '../section-layout/SettingsGroup'
import { matchesSettingsSearch } from '../search/settings-search'
import { getExperimentalSearchEntry } from './experimental-search'
import { HiddenExperimentalGroup } from './HiddenExperimentalGroup'
@@ -55,214 +56,219 @@ export function ExperimentalPane({
)
return (
<div className="space-y-4">
{showAgentDashboard ? (
<AgentDashboardExperimentalSetting settings={settings} updateSettings={updateSettings} />
) : null}
<div className="space-y-8">
<SettingsGroup>
{showAgentDashboard ? (
<AgentDashboardExperimentalSetting settings={settings} updateSettings={updateSettings} />
) : null}
{showPet ? (
<SearchableSetting
title={translate('auto.components.settings.ExperimentalPane.dd6f0a1d45', 'Pet')}
description={translate(
'auto.components.settings.ExperimentalPane.0e89a574ae',
'Floating animated pet in the bottom-right corner.'
)}
keywords={getExperimentalSearchEntry().pet.keywords}
className="space-y-3 py-2"
id="experimental-pet"
>
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 shrink space-y-1.5">
<Label>
{translate('auto.components.settings.ExperimentalPane.dd6f0a1d45', 'Pet')}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.ExperimentalPane.ca2219fe5e',
'Shows a small animated pet pinned to the bottom-right corner. Pick a character (Claudino, OpenCode, Gremlin) or upload your own PNG, APNG, GIF, WebP, JPG, or SVG from the status-bar pet menu. Hide it any time from the same menu without disabling this setting.'
{showPet ? (
<SearchableSetting
title={translate('auto.components.settings.ExperimentalPane.dd6f0a1d45', 'Pet')}
description={translate(
'auto.components.settings.ExperimentalPane.0e89a574ae',
'Floating animated pet in the bottom-right corner.'
)}
keywords={getExperimentalSearchEntry().pet.keywords}
className="space-y-3 py-3"
id="experimental-pet"
>
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 shrink space-y-1.5">
<Label>
{translate('auto.components.settings.ExperimentalPane.dd6f0a1d45', 'Pet')}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.ExperimentalPane.ca2219fe5e',
'Shows a small animated pet pinned to the bottom-right corner. Pick a character (Claudino, OpenCode, Gremlin) or upload your own PNG, APNG, GIF, WebP, JPG, or SVG from the status-bar pet menu. Hide it any time from the same menu without disabling this setting.'
)}
</p>
</div>
<Switch
aria-label={translate(
'auto.components.settings.ExperimentalPane.dd6f0a1d45',
'Pet'
)}
</p>
checked={settings.experimentalPet}
onCheckedChange={(checked) => {
updateSettings({ experimentalPet: checked })
}}
/>
</div>
<Switch
aria-label={translate('auto.components.settings.ExperimentalPane.dd6f0a1d45', 'Pet')}
checked={settings.experimentalPet}
onCheckedChange={(checked) => {
updateSettings({ experimentalPet: checked })
}}
/>
</div>
</SearchableSetting>
) : null}
</SearchableSetting>
) : null}
{showTerminalAttention ? (
<SearchableSetting
title={translate(
'auto.components.settings.ExperimentalPane.ec897e8d89',
'Terminal attention'
)}
description={translate(
'auto.components.settings.ExperimentalPane.88b7613afb',
'Persistent pane highlight for terminal bell and agent-completion events.'
)}
keywords={getExperimentalSearchEntry().terminalAttention.keywords}
className="space-y-3 py-2"
>
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 shrink space-y-0.5">
<Label>
{translate(
{showTerminalAttention ? (
<SearchableSetting
title={translate(
'auto.components.settings.ExperimentalPane.ec897e8d89',
'Terminal attention'
)}
description={translate(
'auto.components.settings.ExperimentalPane.88b7613afb',
'Persistent pane highlight for terminal bell and agent-completion events.'
)}
keywords={getExperimentalSearchEntry().terminalAttention.keywords}
className="space-y-3 py-3"
>
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 shrink space-y-0.5">
<Label>
{translate(
'auto.components.settings.ExperimentalPane.ec897e8d89',
'Terminal attention'
)}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.ExperimentalPane.a20d5ea365',
'Keeps a pane-level highlight visible after terminal bell or agent-completion events until you interact with that pane. Experimental while we tune the signal.'
)}
</p>
</div>
<Switch
aria-label={translate(
'auto.components.settings.ExperimentalPane.ec897e8d89',
'Terminal attention'
)}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.ExperimentalPane.a20d5ea365',
'Keeps a pane-level highlight visible after terminal bell or agent-completion events until you interact with that pane. Experimental while we tune the signal.'
)}
</p>
</div>
<Switch
aria-label={translate(
'auto.components.settings.ExperimentalPane.ec897e8d89',
'Terminal attention'
)}
checked={settings.experimentalTerminalAttention}
onCheckedChange={(checked) =>
updateSettings({
experimentalTerminalAttention: checked
})
}
/>
</div>
</SearchableSetting>
) : null}
{showAgentHibernation ? (
<SearchableSetting
title={translate(
'auto.components.settings.ExperimentalPane.agentHibernation.title',
'Agent sleep'
)}
description={translate(
'auto.components.settings.ExperimentalPane.agentHibernation.description',
'Stops idle background agent terminals after the configured idle window and resumes supported sessions when you open them again.'
)}
keywords={getExperimentalSearchEntry().agentHibernation.keywords}
className="space-y-3 py-2"
id="experimental-agent-hibernation"
>
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 shrink space-y-0.5">
<Label>
{translate(
'auto.components.settings.ExperimentalPane.agentHibernation.title',
'Agent sleep'
)}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.ExperimentalPane.agentHibernation.copy',
'Stops idle background agent terminals after the configured idle window and resumes supported sessions when you open them again. Agent sleep preserves launch options for agents started by Orca. Manually started agents may resume with your current Orca defaults. Experimental while we tune the safety model.'
)}
</p>
</div>
<SettingsSwitch
checked={agentHibernationEnabled}
ariaLabel={translate(
'auto.components.settings.ExperimentalPane.agentHibernation.toggleLabel',
'Toggle agent sleep'
)}
onChange={() =>
updateSettings({
experimentalAgentHibernation: !agentHibernationEnabled
})
}
/>
</div>
{agentHibernationEnabled ? (
<div className="ml-4 space-y-3 border-l border-border pl-4">
<NumberField
className="py-0"
label={translate(
'auto.components.settings.ExperimentalPane.agentHibernation.idleMinutesLabel',
'Sleep after'
)}
description={translate(
'auto.components.settings.ExperimentalPane.agentHibernation.idleMinutesDescription',
'How many idle minutes a completed background agent must wait before Orca can sleep it.'
)}
value={agentHibernationIdleMinutes}
min={MIN_AGENT_HIBERNATION_IDLE_MS / MS_PER_MINUTE}
max={MAX_AGENT_HIBERNATION_IDLE_MS / MS_PER_MINUTE}
step={1}
suffix={translate(
'auto.components.settings.ExperimentalPane.agentHibernation.idleMinutesSuffix',
'minutes'
)}
onChange={(minutes) =>
checked={settings.experimentalTerminalAttention}
onCheckedChange={(checked) =>
updateSettings({
agentHibernationIdleMs: minutes * MS_PER_MINUTE
experimentalTerminalAttention: checked
})
}
/>
</div>
) : null}
</SearchableSetting>
) : null}
</SearchableSetting>
) : null}
{showNewWorktreeCardStyle ? (
<SearchableSetting
title={translate(
'auto.components.settings.ExperimentalPane.newWorktreeCardStyle.title',
'New card style'
)}
description={translate(
'auto.components.settings.ExperimentalPane.newWorktreeCardStyle.description',
'Preview updated worktree-card layout, metadata placement, card-display menu options, and status presentation.'
)}
keywords={getExperimentalSearchEntry().newWorktreeCardStyle.keywords}
className="space-y-3 py-2"
id="experimental-new-worktree-card-style"
>
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 shrink space-y-0.5">
<Label>
{translate(
'auto.components.settings.ExperimentalPane.newWorktreeCardStyle.title',
'New card style'
{showAgentHibernation ? (
<SearchableSetting
title={translate(
'auto.components.settings.ExperimentalPane.agentHibernation.title',
'Agent sleep'
)}
description={translate(
'auto.components.settings.ExperimentalPane.agentHibernation.description',
'Stops idle background agent terminals after the configured idle window and resumes supported sessions when you open them again.'
)}
keywords={getExperimentalSearchEntry().agentHibernation.keywords}
className="space-y-3 py-3"
id="experimental-agent-hibernation"
>
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 shrink space-y-0.5">
<Label>
{translate(
'auto.components.settings.ExperimentalPane.agentHibernation.title',
'Agent sleep'
)}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.ExperimentalPane.agentHibernation.copy',
'Stops idle background agent terminals after the configured idle window and resumes supported sessions when you open them again. Agent sleep preserves launch options for agents started by Orca. Manually started agents may resume with your current Orca defaults. Experimental while we tune the safety model.'
)}
</p>
</div>
<SettingsSwitch
checked={agentHibernationEnabled}
ariaLabel={translate(
'auto.components.settings.ExperimentalPane.agentHibernation.toggleLabel',
'Toggle agent sleep'
)}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.ExperimentalPane.newWorktreeCardStyle.copy',
'Preview updated worktree-card layout, metadata placement, card-display menu options, and status presentation.'
)}
</p>
onChange={() =>
updateSettings({
experimentalAgentHibernation: !agentHibernationEnabled
})
}
/>
</div>
<SettingsSwitch
checked={newWorktreeCardStyleEnabled}
ariaLabel={translate(
'auto.components.settings.ExperimentalPane.newWorktreeCardStyle.toggleLabel',
'Toggle new card style'
)}
onChange={() =>
updateSettings({
experimentalNewWorktreeCardStyle: !newWorktreeCardStyleEnabled
})
}
/>
</div>
</SearchableSetting>
) : null}
{agentHibernationEnabled ? (
<div className="ml-4 space-y-3 border-l border-border pl-4">
<NumberField
className="py-0"
label={translate(
'auto.components.settings.ExperimentalPane.agentHibernation.idleMinutesLabel',
'Sleep after'
)}
description={translate(
'auto.components.settings.ExperimentalPane.agentHibernation.idleMinutesDescription',
'How many idle minutes a completed background agent must wait before Orca can sleep it.'
)}
value={agentHibernationIdleMinutes}
min={MIN_AGENT_HIBERNATION_IDLE_MS / MS_PER_MINUTE}
max={MAX_AGENT_HIBERNATION_IDLE_MS / MS_PER_MINUTE}
step={1}
suffix={translate(
'auto.components.settings.ExperimentalPane.agentHibernation.idleMinutesSuffix',
'minutes'
)}
onChange={(minutes) =>
updateSettings({
agentHibernationIdleMs: minutes * MS_PER_MINUTE
})
}
/>
</div>
) : null}
</SearchableSetting>
) : null}
<EphemeralVmsExperimentalSetting settings={settings} updateSettings={updateSettings} />
{showNewWorktreeCardStyle ? (
<SearchableSetting
title={translate(
'auto.components.settings.ExperimentalPane.newWorktreeCardStyle.title',
'New card style'
)}
description={translate(
'auto.components.settings.ExperimentalPane.newWorktreeCardStyle.description',
'Preview updated worktree-card layout, metadata placement, card-display menu options, and status presentation.'
)}
keywords={getExperimentalSearchEntry().newWorktreeCardStyle.keywords}
className="space-y-3 py-3"
id="experimental-new-worktree-card-style"
>
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 shrink space-y-0.5">
<Label>
{translate(
'auto.components.settings.ExperimentalPane.newWorktreeCardStyle.title',
'New card style'
)}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.ExperimentalPane.newWorktreeCardStyle.copy',
'Preview updated worktree-card layout, metadata placement, card-display menu options, and status presentation.'
)}
</p>
</div>
<SettingsSwitch
checked={newWorktreeCardStyleEnabled}
ariaLabel={translate(
'auto.components.settings.ExperimentalPane.newWorktreeCardStyle.toggleLabel',
'Toggle new card style'
)}
onChange={() =>
updateSettings({
experimentalNewWorktreeCardStyle: !newWorktreeCardStyleEnabled
})
}
/>
</div>
</SearchableSetting>
) : null}
<BrowserExtensionsExperimentalSetting settings={settings} updateSettings={updateSettings} />
<EphemeralVmsExperimentalSetting settings={settings} updateSettings={updateSettings} />
{wslHostsAvailable() ? (
<WslHostsExperimentalSetting settings={settings} updateSettings={updateSettings} />
) : null}
<BrowserExtensionsExperimentalSetting settings={settings} updateSettings={updateSettings} />
{wslHostsAvailable() ? (
<WslHostsExperimentalSetting settings={settings} updateSettings={updateSettings} />
) : null}
</SettingsGroup>
{hiddenExperimentalUnlocked ? <HiddenExperimentalGroup /> : null}
</div>
@@ -244,7 +244,7 @@ export function AdvancedNetworkSettingsSection({
'mirror',
'nodejs'
]}
className="space-y-3"
className="space-y-3 py-3"
>
<div className="flex items-center justify-between gap-4">
<div className="min-w-0 space-y-0.5">
@@ -22,7 +22,7 @@ import {
getAgentWorkspaceTrustDescription,
getAgentWorkspaceTrustTitle
} from './behavior/agent-workspace-trust-copy'
import { SettingsSubsectionHeader, SettingsSwitchRow } from '../form-controls/SettingsFormControls'
import { SettingsSwitchRow } from '../form-controls/SettingsFormControls'
import { isPairedWebClientWindow } from '@/lib/desktop-window-chrome'
import { AgentLaunchSettingsSection } from './launch/AgentLaunchSettingsSection'
@@ -31,6 +31,7 @@ import {
type AgentPermissionsSettingProps
} from './permissions/AgentPermissionsSetting'
import type { SettingsHostScope } from '../host-scope/settings-host-scope'
import { SettingsGroup } from '../section-layout/SettingsGroup'
type AgentSettingProps = {
settings: GlobalSettings
@@ -63,15 +64,14 @@ export function AgentsPane({
hostScope={hostScope}
renderPermissions={renderAgentPermissions}
/>
<section className="space-y-3">
<SettingsGroup>
<CodexHistorySharingSetting
settings={settings}
updateSettings={updateSettings}
hostScope={hostScope}
/>
</section>
<section className="space-y-8">
<SettingsSubsectionHeader title={getLocalExecutionHostLabel()} />
</SettingsGroup>
<SettingsGroup title={getLocalExecutionHostLabel()}>
<AgentRuntimeSetting
settings={settings}
updateSettings={updateSettings}
@@ -108,8 +108,8 @@ export function AgentsPane({
{!isPairedWebClientWindow() ? (
<AgentAwakeSetting settings={settings} updateSettings={updateSettings} />
) : null}
<AgentCacheTimerSection settings={settings} updateSettings={updateSettings} />
</section>
</SettingsGroup>
<AgentCacheTimerSection settings={settings} updateSettings={updateSettings} />
</div>
)
}
@@ -121,44 +121,38 @@ function renderAgentPermissions(props: AgentPermissionsSettingProps): React.Reac
export function AgentStatusHooksSetting({ settings, updateSettings }: AgentSettingProps) {
const enabled = settings.agentStatusHooksEnabled !== false
return (
<section className="space-y-3">
<SettingsSwitchRow
label={getAgentStatusHooksTitle()}
description={getAgentStatusHooksDescription()}
checked={enabled}
onChange={() => updateSettings({ agentStatusHooksEnabled: !enabled })}
ariaLabel={getAgentStatusHooksTitle()}
/>
</section>
<SettingsSwitchRow
label={getAgentStatusHooksTitle()}
description={getAgentStatusHooksDescription()}
checked={enabled}
onChange={() => updateSettings({ agentStatusHooksEnabled: !enabled })}
ariaLabel={getAgentStatusHooksTitle()}
/>
)
}
export function AgentWorkspaceTrustSetting({ settings, updateSettings }: AgentSettingProps) {
const enabled = settings.agentWorkspaceTrustEnabled !== false
return (
<section className="space-y-3">
<SettingsSwitchRow
label={getAgentWorkspaceTrustTitle()}
description={getAgentWorkspaceTrustDescription()}
checked={enabled}
onChange={() => updateSettings({ agentWorkspaceTrustEnabled: !enabled })}
ariaLabel={getAgentWorkspaceTrustTitle()}
/>
</section>
<SettingsSwitchRow
label={getAgentWorkspaceTrustTitle()}
description={getAgentWorkspaceTrustDescription()}
checked={enabled}
onChange={() => updateSettings({ agentWorkspaceTrustEnabled: !enabled })}
ariaLabel={getAgentWorkspaceTrustTitle()}
/>
)
}
export function AgentGeneratedTabTitlesSetting({ settings, updateSettings }: AgentSettingProps) {
const enabled = settings.tabAutoGenerateTitle === true
return (
<section className="space-y-3">
<SettingsSwitchRow
label={getAgentGeneratedTabTitlesTitle()}
description={getAgentGeneratedTabTitlesDescription()}
checked={enabled}
onChange={() => updateSettings({ tabAutoGenerateTitle: !enabled })}
ariaLabel={getAgentGeneratedTabTitlesTitle()}
/>
</section>
<SettingsSwitchRow
label={getAgentGeneratedTabTitlesTitle()}
description={getAgentGeneratedTabTitlesDescription()}
checked={enabled}
onChange={() => updateSettings({ tabAutoGenerateTitle: !enabled })}
ariaLabel={getAgentGeneratedTabTitlesTitle()}
/>
)
}
@@ -34,39 +34,37 @@ export function AgentAwakeSetting({
}
return (
<section className="space-y-3">
<SearchableSetting
title={title}
description={description}
keywords={getAgentAwakeSearchKeywords()}
>
<div className="flex items-start justify-between gap-4 py-2">
<div className="min-w-0 flex-1 space-y-0.5">
<Label>{title}</Label>
<p className="text-xs text-muted-foreground">{description}</p>
</div>
<SettingsSegmentedControl
value={mode}
onChange={setMode}
ariaLabel={title}
size="sm"
options={[
{
value: 'on',
label: getAgentAwakeModeLabel('on')
},
{
value: 'auto',
label: getAgentAwakeModeLabel('auto')
},
{
value: 'off',
label: getAgentAwakeModeLabel('off')
}
]}
/>
<SearchableSetting
title={title}
description={description}
keywords={getAgentAwakeSearchKeywords()}
>
<div className="flex items-start justify-between gap-4 py-3">
<div className="min-w-0 flex-1 space-y-0.5">
<Label>{title}</Label>
<p className="text-xs text-muted-foreground">{description}</p>
</div>
</SearchableSetting>
</section>
<SettingsSegmentedControl
value={mode}
onChange={setMode}
ariaLabel={title}
size="sm"
options={[
{
value: 'on',
label: getAgentAwakeModeLabel('on')
},
{
value: 'auto',
label: getAgentAwakeModeLabel('auto')
},
{
value: 'off',
label: getAgentAwakeModeLabel('off')
}
]}
/>
</div>
</SearchableSetting>
)
}
@@ -6,8 +6,9 @@ import { Label } from '../../../ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../ui/select'
import { getAgentCacheTimerSearchEntries } from './agent-cache-timer-search'
import { SearchableSetting } from '../../section-layout/SearchableSetting'
import { SettingsSubsectionHeader, SettingsSwitch } from '../../form-controls/SettingsFormControls'
import { SettingsSwitch } from '../../form-controls/SettingsFormControls'
import { translate } from '@/i18n/i18n'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
type AgentCacheTimerSectionProps = {
settings: GlobalSettings
@@ -19,18 +20,16 @@ export function AgentCacheTimerSection({
updateSettings
}: AgentCacheTimerSectionProps): React.JSX.Element {
return (
<section className="space-y-4">
<SettingsSubsectionHeader
title={translate(
'auto.components.settings.AgentCacheTimerSection.a137f8854d',
'Prompt Cache Timer'
)}
description={translate(
'auto.components.settings.AgentCacheTimerSection.fe590653c1',
'Claude caches your conversation to reduce costs. When idle too long the cache expires and the next message resends full context at higher cost. This shows a countdown so you know when to resume.'
)}
/>
<SettingsGroup
title={translate(
'auto.components.settings.AgentCacheTimerSection.a137f8854d',
'Prompt Cache Timer'
)}
description={translate(
'auto.components.settings.AgentCacheTimerSection.fe590653c1',
'Claude caches your conversation to reduce costs. When idle too long the cache expires and the next message resends full context at higher cost. This shows a countdown so you know when to resume.'
)}
>
<SearchableSetting
title={translate(
'auto.components.settings.AgentCacheTimerSection.b4e7302944',
@@ -45,7 +44,7 @@ export function AgentCacheTimerSection({
entry.description ?? '',
...(entry.keywords ?? [])
])}
className="flex items-center justify-between gap-4 py-2"
className="flex items-center justify-between gap-4 py-3"
>
<div className="min-w-0 flex-1 space-y-0.5">
<div className="flex items-center gap-2">
@@ -91,7 +90,7 @@ export function AgentCacheTimerSection({
"Match this to your provider's cache TTL."
)}
keywords={['cache', 'timer', 'duration', 'ttl']}
className="flex items-center justify-between gap-4 py-2 pl-7"
className="flex items-center justify-between gap-4 py-3 pl-7"
>
<div className="min-w-0 flex-1 space-y-0.5">
<Label>
@@ -128,6 +127,6 @@ export function AgentCacheTimerSection({
</Select>
</SearchableSetting>
)}
</section>
</SettingsGroup>
)
}
@@ -4,7 +4,7 @@ import type { TuiAgent } from '../../../../../../shared/agent-launch/catalog/tui
import { type AgentCatalogEntry, AgentIcon } from '@/lib/agent-catalog'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { SettingsSubsectionHeader } from '../../form-controls/SettingsFormControls'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
function DefaultAgentPill({
active,
@@ -42,7 +42,8 @@ export function AgentDefaultSetting({
catalog,
description,
onSetDefault,
defaultOpensAsChat = false
defaultOpensAsChat = false,
permissions
}: {
defaultAgent: TuiAgent | 'blank' | null
detectedIds: Set<string> | null
@@ -52,6 +53,7 @@ export function AgentDefaultSetting({
catalog: AgentCatalogEntry[]
description: string
onSetDefault: (agent: TuiAgent | 'blank' | null) => void
permissions: React.ReactNode
}): React.JSX.Element {
const storedDefaultAgent =
defaultAgent !== null && defaultAgent !== 'blank'
@@ -63,12 +65,11 @@ export function AgentDefaultSetting({
: enabledDetectedAgents
return (
<section className="space-y-4">
<SettingsSubsectionHeader
title={translate('auto.components.settings.AgentsPane.385212c7a1', 'Default Agent')}
description={description}
/>
<div className="flex flex-wrap gap-2">
<SettingsGroup
title={translate('auto.components.settings.AgentsPane.385212c7a1', 'Default Agent')}
description={description}
>
<div className="flex flex-wrap gap-2 py-4">
<DefaultAgentPill
active={defaultAgent === null}
onClick={() => onSetDefault(defaultAgent === null ? 'blank' : null)}
@@ -101,6 +102,9 @@ export function AgentDefaultSetting({
)
})}
</div>
</section>
<div data-settings-row="" className="py-3">
{permissions}
</div>
</SettingsGroup>
)
}
@@ -5,7 +5,8 @@ import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { Button } from '../../../ui/button'
import { AgentCatalogRow, type AgentCatalogRowProps } from './AgentCatalogRow'
import { SettingsBadge, SettingsSubsectionHeader } from '../../form-controls/SettingsFormControls'
import { SettingsBadge } from '../../form-controls/SettingsFormControls'
import { SettingsGroupHeader } from '../../section-layout/SettingsGroup'
import { AGENTS_INSTALLED_SECTION_ID } from '../agents-search'
export function AgentDetectionCatalog({
@@ -43,95 +44,98 @@ export function AgentDetectionCatalog({
</div>
)}
{/* Focusable so "Choose per agent" can move focus here. */}
{/* Focusable so "Choose per agent" can move focus here. One keyed list (not one SettingsGroup per
section) so a row keeps its state, e.g. expanded setup, when it moves from available to installed. */}
<section id={AGENTS_INSTALLED_SECTION_ID} tabIndex={-1} className="scroll-mt-8 outline-none">
{[
...detectedAgents.map((agent) => ({ agent, installed: true })),
...undetectedAgents.map((agent) => ({ agent, installed: false }))
].map(({ agent, installed }, index) => {
const firstInSection = index === 0 || index === detectedAgents.length
const sectionLength = installed ? detectedAgents.length : undetectedAgents.length
const indexInSection = installed ? index : index - detectedAgents.length
const firstInSection = indexInSection === 0
const lastInSection = indexInSection === sectionLength - 1
return (
<Fragment key={agent.id}>
{firstInSection ? (
<div className={cn('mb-3', index > 0 && 'mt-8')}>
{installed ? (
<SettingsSubsectionHeader
title={
<span className="flex items-center gap-2">
{translate('auto.components.settings.AgentsPane.02e0143be5', 'Installed')}
<SettingsBadge tone="accent">
{detectedAgents.length}{' '}
<SettingsGroupHeader
className={cn('mb-3', index > 0 && 'mt-8')}
title={
installed ? (
<span className="flex items-center gap-2">
{translate('auto.components.settings.AgentsPane.02e0143be5', 'Installed')}
<SettingsBadge tone="accent">
{detectedAgents.length}{' '}
{translate('auto.components.settings.AgentsPane.ed3e110e61', 'detected')}
</SettingsBadge>
{activeServerName ? (
<SettingsBadge tone="muted">
{translate(
'auto.components.settings.AgentsPane.ed3e110e61',
'detected'
'auto.components.settings.AgentsPane.03e1a5081a',
'on {{value0}}',
{
value0: activeServerName
}
)}
</SettingsBadge>
{activeServerName ? (
<SettingsBadge tone="muted">
{translate(
'auto.components.settings.AgentsPane.03e1a5081a',
'on {{value0}}',
{
value0: activeServerName
}
)}
</SettingsBadge>
) : null}
</span>
}
action={
<Button
type="button"
variant="ghost"
size="xs"
onClick={onRefresh}
disabled={isRefreshing}
title={
activeServerEnvironmentId
? translate(
'auto.components.settings.AgentsPane.25a41a9aad',
'Re-detect agents installed on this server'
)
: translate(
'auto.components.settings.AgentsPane.13647f9f80',
'Re-read your shell PATH and re-detect installed agents'
)
}
className="h-7 gap-1.5 text-xs text-muted-foreground hover:text-foreground"
>
<RefreshCw className={cn('size-3', isRefreshing && 'animate-spin')} />
{isRefreshing
) : null}
</span>
) : (
<span className="flex items-center gap-2 text-muted-foreground">
{translate(
'auto.components.settings.AgentsPane.e8da2af684',
'Available to install'
)}
<SettingsBadge tone="muted">
{undetectedAgents.length}{' '}
{translate('auto.components.settings.AgentsPane.024bd95089', 'agents')}
</SettingsBadge>
</span>
)
}
action={
installed ? (
<Button
type="button"
variant="ghost"
size="xs"
onClick={onRefresh}
disabled={isRefreshing}
title={
activeServerEnvironmentId
? translate(
'auto.components.settings.AgentsPane.c9b33eb5c0',
'Refreshing…'
'auto.components.settings.AgentsPane.25a41a9aad',
'Re-detect agents installed on this server'
)
: translate(
'auto.components.settings.AgentsPane.0d9e293a02',
'Refresh'
)}
</Button>
}
/>
) : (
<SettingsSubsectionHeader
title={
<span className="flex items-center gap-2 text-muted-foreground">
{translate(
'auto.components.settings.AgentsPane.e8da2af684',
'Available to install'
)}
<SettingsBadge tone="muted">
{undetectedAgents.length}{' '}
{translate('auto.components.settings.AgentsPane.024bd95089', 'agents')}
</SettingsBadge>
</span>
}
/>
)}
</div>
'auto.components.settings.AgentsPane.13647f9f80',
'Re-read your shell PATH and re-detect installed agents'
)
}
className="h-7 gap-1.5 text-xs text-muted-foreground hover:text-foreground"
>
<RefreshCw className={cn('size-3', isRefreshing && 'animate-spin')} />
{isRefreshing
? translate(
'auto.components.settings.AgentsPane.c9b33eb5c0',
'Refreshing…'
)
: translate('auto.components.settings.AgentsPane.0d9e293a02', 'Refresh')}
</Button>
) : undefined
}
/>
) : null}
<div className={cn(!firstInSection && 'border-t border-border/40')}>
<AgentCatalogRow {...getRowProps(agent, installed)} />
<div
className={cn(
'border-x border-border bg-card/50 px-5',
firstInSection && 'rounded-t-xl border-t',
lastInSection && 'rounded-b-xl border-b shadow-xs'
)}
>
<div className={cn(!firstInSection && 'border-t border-border')}>
<AgentCatalogRow {...getRowProps(agent, installed)} />
</div>
</div>
</Fragment>
)
@@ -336,33 +336,36 @@ export function AgentLaunchSettingsCatalog({
)}
onSetDefault={(agent) => save({ type: 'default', agent })}
defaultOpensAsChat={defaultOpensAsChat}
permissions={
permissionSettings ? (
<AllAgentsPermissionSetting
chatOwnSupported={chatOwnSupported}
choice={permissionSettings.agentPermissionDefault ?? 'own'}
withoutAutoChoice={sharedChoice(
PERMISSION_CHOICE_AGENTS.filter(
(agent) => !agentPermissionChoices(agent).includes('auto')
).map((agent) => resolveAgentPermission(agent, permissionSettings).choice)
)}
onChange={(choice) => save({ type: 'permission', choice })}
onChangeWithoutAuto={(choice) =>
save({ type: 'permission', choice, withoutAuto: true })
}
/>
) : (
renderPermissions({
mode: permissions.globalMode,
nonAutoMode: settings.nonAutoPermissionMode,
majorityMode: permissions.majorityMode,
autoAgentLabels: catalog
.filter((agent) => agent.id in AUTO_TUI_AGENT_ARGS)
.map((agent) => agent.label),
onChange: (mode) => save({ type: 'permissions', mode }),
onChangeNonAuto: (mode) => save({ type: 'permissions', mode, exceptAuto: true }),
autoSupported
})
)
}
/>
{permissionSettings ? (
<AllAgentsPermissionSetting
chatOwnSupported={chatOwnSupported}
choice={permissionSettings.agentPermissionDefault ?? 'own'}
withoutAutoChoice={sharedChoice(
PERMISSION_CHOICE_AGENTS.filter(
(agent) => !agentPermissionChoices(agent).includes('auto')
).map((agent) => resolveAgentPermission(agent, permissionSettings).choice)
)}
onChange={(choice) => save({ type: 'permission', choice })}
onChangeWithoutAuto={(choice) => save({ type: 'permission', choice, withoutAuto: true })}
/>
) : null}
{permissionSettings
? null
: renderPermissions({
mode: permissions.globalMode,
nonAutoMode: settings.nonAutoPermissionMode,
majorityMode: permissions.majorityMode,
autoAgentLabels: catalog
.filter((agent) => agent.id in AUTO_TUI_AGENT_ARGS)
.map((agent) => agent.label),
onChange: (mode) => save({ type: 'permissions', mode }),
onChangeNonAuto: (mode) => save({ type: 'permissions', mode, exceptAuto: true }),
autoSupported
})}
<AgentDetectionCatalog
detectedAgents={installedAgents}
undetectedAgents={undetectedAgents}
@@ -15,12 +15,14 @@ export function AgentLaunchSettingsLoading(): React.JSX.Element {
'Defaults for new agents on the selected host.'
)}
onSetDefault={() => {}}
/>
<AllAgentsPermissionSetting
choice={null}
withoutAutoChoice={null}
onChange={() => {}}
onChangeWithoutAuto={() => {}}
permissions={
<AllAgentsPermissionSetting
choice={null}
withoutAutoChoice={null}
onChange={() => {}}
onChangeWithoutAuto={() => {}}
/>
}
/>
</fieldset>
)
@@ -61,81 +61,79 @@ export function AgentRuntimeSetting({
}
return (
<section className="space-y-3">
<SettingsRow
label={translate('auto.components.settings.AgentRuntimeSetting.label', 'Agent runtime')}
alignTop
description={getDescription(runtimeDefault, wslAvailable, wslCapabilitiesLoading)}
control={
<div className="flex w-52 flex-col items-stretch gap-2">
<SettingsSegmentedControl<AgentRuntimeSegment>
ariaLabel={translate(
'auto.components.settings.AgentRuntimeSetting.label',
'Agent runtime'
)}
value={runtimeDefault.kind}
onChange={handleRuntimeChange}
equalWidth
options={[
{
value: 'windows-host',
label: getHostRuntimeLabel()
},
{
value: 'wsl',
label: translate('auto.components.settings.AgentRuntimeSetting.wsl', 'WSL'),
disabled: wslCapabilitiesLoading || !wslAvailable || !nextWslDistro
<SettingsRow
label={translate('auto.components.settings.AgentRuntimeSetting.label', 'Agent runtime')}
alignTop
description={getDescription(runtimeDefault, wslAvailable, wslCapabilitiesLoading)}
control={
<div className="flex w-52 flex-col items-stretch gap-2">
<SettingsSegmentedControl<AgentRuntimeSegment>
ariaLabel={translate(
'auto.components.settings.AgentRuntimeSetting.label',
'Agent runtime'
)}
value={runtimeDefault.kind}
onChange={handleRuntimeChange}
equalWidth
options={[
{
value: 'windows-host',
label: getHostRuntimeLabel()
},
{
value: 'wsl',
label: translate('auto.components.settings.AgentRuntimeSetting.wsl', 'WSL'),
disabled: wslCapabilitiesLoading || !wslAvailable || !nextWslDistro
}
]}
/>
{runtimeDefault.kind === 'wsl' ? (
<Select
value={runtimeDefault.distro ?? NO_DISTRO_VALUE}
onValueChange={(distro) => {
if (distro !== NO_DISTRO_VALUE) {
updateAgentRuntime({
localWindowsRuntimeDefault: { kind: 'wsl', distro }
})
}
]}
/>
{runtimeDefault.kind === 'wsl' ? (
<Select
value={runtimeDefault.distro ?? NO_DISTRO_VALUE}
onValueChange={(distro) => {
if (distro !== NO_DISTRO_VALUE) {
updateAgentRuntime({
localWindowsRuntimeDefault: { kind: 'wsl', distro }
})
}}
disabled={wslCapabilitiesLoading || !wslAvailable}
>
<SelectTrigger size="sm" className="w-full min-w-52">
<SelectValue
placeholder={
wslCapabilitiesLoading
? translate(
'auto.components.settings.AgentRuntimeSetting.loadingWsl',
'Loading WSL'
)
: translate(
'auto.components.settings.AgentRuntimeSetting.selectDistro',
'Select distro'
)
}
}}
disabled={wslCapabilitiesLoading || !wslAvailable}
>
<SelectTrigger size="sm" className="w-full min-w-52">
<SelectValue
placeholder={
wslCapabilitiesLoading
? translate(
'auto.components.settings.AgentRuntimeSetting.loadingWsl',
'Loading WSL'
)
: translate(
'auto.components.settings.AgentRuntimeSetting.selectDistro',
'Select distro'
)
}
/>
</SelectTrigger>
<SelectContent>
{!runtimeDefault.distro ? (
<SelectItem value={NO_DISTRO_VALUE}>
{translate(
'auto.components.settings.AgentRuntimeSetting.selectDistro',
'Select distro'
)}
</SelectItem>
) : null}
{distroOptions.map((distro) => (
<SelectItem key={distro} value={distro}>
{distro}
</SelectItem>
))}
</SelectContent>
</Select>
) : null}
</div>
}
/>
</section>
/>
</SelectTrigger>
<SelectContent>
{!runtimeDefault.distro ? (
<SelectItem value={NO_DISTRO_VALUE}>
{translate(
'auto.components.settings.AgentRuntimeSetting.selectDistro',
'Select distro'
)}
</SelectItem>
) : null}
{distroOptions.map((distro) => (
<SelectItem key={distro} value={distro}>
{distro}
</SelectItem>
))}
</SelectContent>
</Select>
) : null}
</div>
}
/>
)
}
@@ -27,33 +27,33 @@ export function CodexTerminalServerIsolationSetting({
const description = getCodexTerminalServerIsolationDescription()
const enabled = isCodexTerminalServerIsolationEnabled(settings)
return (
<section className="space-y-3">
<SearchableSetting
id={CODEX_TERMINAL_SERVER_ISOLATION_SETTINGS_TARGET_ID}
title={title}
<SearchableSetting
id={CODEX_TERMINAL_SERVER_ISOLATION_SETTINGS_TARGET_ID}
title={title}
description={description}
keywords={getCodexTerminalServerIsolationSearchKeywords()}
// Two independent switches share one search unit, so split them like separate rows.
className="divide-y divide-border"
>
<SettingsSwitchRow
label={title}
description={description}
keywords={getCodexTerminalServerIsolationSearchKeywords()}
>
checked={enabled}
onChange={() => void updateSettings({ codexTerminalServerIsolation: !enabled })}
/>
{/* With isolation off, sharing is intentional and needs no warning control. */}
{enabled ? (
<SettingsSwitchRow
label={title}
description={description}
checked={enabled}
onChange={() => void updateSettings({ codexTerminalServerIsolation: !enabled })}
label={getCodexSharedServerWarningTitle()}
description={getCodexSharedServerWarningDescription()}
checked={isCodexSharedServerWarningEnabled(settings)}
onChange={() =>
void updateSettings({
codexSharedServerWarning: !isCodexSharedServerWarningEnabled(settings)
})
}
/>
{/* With isolation off, sharing is intentional and needs no warning control. */}
{enabled ? (
<SettingsSwitchRow
label={getCodexSharedServerWarningTitle()}
description={getCodexSharedServerWarningDescription()}
checked={isCodexSharedServerWarningEnabled(settings)}
onChange={() =>
void updateSettings({
codexSharedServerWarning: !isCodexSharedServerWarningEnabled(settings)
})
}
/>
) : null}
</SearchableSetting>
</section>
) : null}
</SearchableSetting>
)
}
@@ -20,6 +20,7 @@ import {
import { AGENT_DEFAULT_SURFACE_SECTION_ID, AGENTS_INSTALLED_SECTION_ID } from '../agents-search'
import { Card, CardContent } from '../../../ui/card'
import { SettingsRow } from '../../form-controls/SettingsFormControls'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
import { Button } from '../../../ui/button'
import {
agentsKeepingOwnSurface,
@@ -240,55 +241,57 @@ export function AgentDefaultSurfaceSection({
)
}
return (
<section id={AGENT_DEFAULT_SURFACE_SECTION_ID} className="@container scroll-mt-8">
<h3 className="text-lg font-semibold tracking-tight">{title}</h3>
<p className="mt-1 text-xs text-muted-foreground">
{translate(
<div id={AGENT_DEFAULT_SURFACE_SECTION_ID} className="@container scroll-mt-8">
<SettingsGroup
title={title}
description={translate(
'settings.agents.defaultSurfaceDescription',
'The default for agents on this computer.'
)}
</p>
<div
role="radiogroup"
aria-label={title}
aria-busy={pending}
className="mt-3.5 grid gap-3.5 @3xl:grid-cols-2"
bodyClassName="py-4"
>
<SurfaceTile
name={name}
pending={pending}
checked={surface === 'chat'}
title={translate('settings.agents.defaultOpensAsChat', 'Chat')}
description={translate(
'settings.agents.defaultSurfaceChatDescription',
'Rich messages, visuals and images.'
)}
preview={<ChatSurfacePreview />}
onSelect={() => choose('chat')}
/>
<SurfaceTile
name={name}
pending={pending}
checked={surface === 'terminal'}
title={translate('settings.agents.defaultSurfaceTerminal', 'Terminal')}
description={translate(
'settings.agents.defaultSurfaceTerminalDescription',
'Each agent’s own CLI.'
)}
preview={<TerminalSurfacePreview settings={settings} />}
onSelect={() => choose('terminal')}
/>
</div>
<div className="mt-3 flex justify-end">
<button
type="button"
onClick={showInstalledList}
className="inline-flex items-center gap-0.5 rounded-sm text-[13px] font-medium underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
<div
role="radiogroup"
aria-label={title}
aria-busy={pending}
className="grid gap-3.5 @3xl:grid-cols-2"
>
{translate('settings.agents.defaultSurfacePerAgent', 'Choose per agent')}
<ChevronRight className="size-3.5" />
</button>
</div>
</section>
<SurfaceTile
name={name}
pending={pending}
checked={surface === 'chat'}
title={translate('settings.agents.defaultOpensAsChat', 'Chat')}
description={translate(
'settings.agents.defaultSurfaceChatDescription',
'Rich messages, visuals and images.'
)}
preview={<ChatSurfacePreview />}
onSelect={() => choose('chat')}
/>
<SurfaceTile
name={name}
pending={pending}
checked={surface === 'terminal'}
title={translate('settings.agents.defaultSurfaceTerminal', 'Terminal')}
description={translate(
'settings.agents.defaultSurfaceTerminalDescription',
'Each agent’s own CLI.'
)}
preview={<TerminalSurfacePreview settings={settings} />}
onSelect={() => choose('terminal')}
/>
</div>
<div className="mt-3 flex justify-end">
<button
type="button"
onClick={showInstalledList}
className="inline-flex items-center gap-0.5 rounded-sm text-[13px] font-medium underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
>
{translate('settings.agents.defaultSurfacePerAgent', 'Choose per agent')}
<ChevronRight className="size-3.5" />
</button>
</div>
</SettingsGroup>
</div>
)
}
@@ -8,6 +8,7 @@ import { AppearanceSection } from './AppearanceSection'
import { AppearanceInterfaceSection } from './AppearanceInterfaceSection'
import { AppearanceWindowSidebarSection } from './AppearanceWindowSidebarSection'
import { SearchableSetting } from '../section-layout/SearchableSetting'
import { SettingsGroup } from '../section-layout/SettingsGroup'
import { matchesSettingsSearch, normalizeSettingsSearchQuery } from '../search/settings-search'
import { useAppStore } from '../../../store/index'
import { USAGE_PERCENTAGE_DISPLAY_SETTING_ID } from './appearance-usage-percentage-search'
@@ -269,24 +270,26 @@ export function AppearancePane({
{/* Keep the App Icon easter egg at the bottom instead of inside Interface advanced. */}
{appIconMatches ? (
<SearchableSetting
title={translate('auto.components.settings.AppearancePane.ca1590d42f', 'App Icon')}
description={translate(
'auto.components.settings.AppearancePane.0cd9b8228f',
'Choose the app icon shown in the Dock and window switcher.'
)}
keywords={getAppIconEntries().flatMap((entry) => [
entry.title,
entry.description ?? '',
...(entry.keywords ?? [])
])}
className="max-w-none px-1 pt-2"
>
<AppIconSelector
value={normalizeAppIconId(settings.appIcon)}
onChange={(appIcon) => updateSettings({ appIcon })}
/>
</SearchableSetting>
<SettingsGroup>
<SearchableSetting
title={translate('auto.components.settings.AppearancePane.ca1590d42f', 'App Icon')}
description={translate(
'auto.components.settings.AppearancePane.0cd9b8228f',
'Choose the app icon shown in the Dock and window switcher.'
)}
keywords={getAppIconEntries().flatMap((entry) => [
entry.title,
entry.description ?? '',
...(entry.keywords ?? [])
])}
className="py-4"
>
<AppIconSelector
value={normalizeAppIconId(settings.appIcon)}
onChange={(appIcon) => updateSettings({ appIcon })}
/>
</SearchableSetting>
</SettingsGroup>
) : null}
</div>
)
@@ -2,6 +2,7 @@ import { ArrowRight, Files } from 'lucide-react'
import type { GlobalSettings } from '../../../../../shared/global-settings-types'
import { Button } from '@/components/ui/button'
import { SettingsSwitchRow } from '../form-controls/SettingsFormControls'
import { SettingsGroup } from '../section-layout/SettingsGroup'
import { useOrcaProfileAuthStatusRefresh } from '@/hooks/use-orca-profile-auth-status-refresh'
import { useAppStore } from '@/store/index'
import { isWebClientLocation } from '@/lib/web-client-location'
@@ -77,115 +78,123 @@ export function ArtifactsSettingsPane({
]
return (
<div className="divide-y divide-border">
<SettingsSwitchRow
label={translate(
'auto.components.settings.artifacts.allowPublishing',
'Allow publishing public artifact links'
)}
description={
isWebClient
? translate(
'auto.components.settings.artifacts.allowPublishingWebDescription',
'Desktop only. Open Settings → Artifacts on the host device to change this setting.'
)
: translate(
'auto.components.settings.artifacts.allowPublishingDescription',
'Publish HTML and Markdown files as links anyone with the URL can open. Existing links remain until you delete them from Artifacts.'
)
}
checked={sharingEnabled}
disabled={isWebClient}
onChange={() => void updateSettings({ artifactSharingEnabled: !sharingEnabled })}
/>
<SettingsSwitchRow
label={translate('auto.components.settings.artifacts.showButton', 'Show Artifacts Button')}
description={translate(
'auto.components.settings.artifacts.showButtonDescription',
'Show the Artifacts shortcut in the sidebar.'
)}
checked={settings.showArtifactsButton === true}
onChange={() => void updateSettings({ showArtifactsButton: !settings.showArtifactsButton })}
/>
{!signedIn ? (
<section className="flex flex-wrap items-center gap-4 py-5">
<div className="min-w-0 flex-1 space-y-1">
<h3 className="text-sm font-medium">
{translate(
'auto.components.settings.artifacts.signInTitle',
'Sign in to share artifacts'
)}
</h3>
<p className="text-xs leading-relaxed text-muted-foreground">
{translate(
'auto.components.settings.artifacts.signInDescription',
'Use your Orca account to upload artifacts and manage their public links.'
)}
</p>
</div>
<OrcaSignInButton size="sm" disabled={authStatus?.configured !== true}>
{authStatus?.state === 'reconnect-required'
? translate('auto.components.settings.artifacts.signInAgain', 'Sign in again')
: translate('auto.components.settings.artifacts.signIn', 'Sign in to Orca')}
</OrcaSignInButton>
<OrcaSignInWaitNotice className="basis-full" />
</section>
) : null}
<section className="space-y-4 py-5">
<div className="space-y-1">
<h3 className="text-sm font-medium">
{translate('auto.components.settings.artifacts.howToTitle', 'How to use Artifacts')}
</h3>
<p className="text-xs leading-relaxed text-muted-foreground">
{sharingEnabled
<div className="space-y-8">
<SettingsGroup>
<SettingsSwitchRow
label={translate(
'auto.components.settings.artifacts.allowPublishing',
'Allow publishing public artifact links'
)}
description={
isWebClient
? translate(
'auto.components.settings.artifacts.howToDescription',
'Publish HTML or Markdown files as public links, then share them with your team.'
'auto.components.settings.artifacts.allowPublishingWebDescription',
'Desktop only. Open Settings → Artifacts on the host device to change this setting.'
)
: translate(
'auto.components.settings.artifacts.howToDescriptionDisabled',
'Enable artifact sharing above to publish HTML or Markdown files as public links.'
'auto.components.settings.artifacts.allowPublishingDescription',
'Publish HTML and Markdown files as links anyone with the URL can open. Existing links remain until you delete them from Artifacts.'
)
}
checked={sharingEnabled}
disabled={isWebClient}
onChange={() => void updateSettings({ artifactSharingEnabled: !sharingEnabled })}
/>
<SettingsSwitchRow
label={translate(
'auto.components.settings.artifacts.showButton',
'Show Artifacts Button'
)}
description={translate(
'auto.components.settings.artifacts.showButtonDescription',
'Show the Artifacts shortcut in the sidebar.'
)}
checked={settings.showArtifactsButton === true}
onChange={() =>
void updateSettings({ showArtifactsButton: !settings.showArtifactsButton })
}
/>
{!signedIn ? (
<section data-settings-row="" className="flex flex-wrap items-center gap-4 py-3">
<div className="min-w-0 flex-1 space-y-1">
<h3 className="text-sm font-medium">
{translate(
'auto.components.settings.artifacts.signInTitle',
'Sign in to share artifacts'
)}
</p>
</div>
</h3>
<p className="text-xs leading-relaxed text-muted-foreground">
{translate(
'auto.components.settings.artifacts.signInDescription',
'Use your Orca account to upload artifacts and manage their public links.'
)}
</p>
</div>
<OrcaSignInButton size="sm" disabled={authStatus?.configured !== true}>
{authStatus?.state === 'reconnect-required'
? translate('auto.components.settings.artifacts.signInAgain', 'Sign in again')
: translate('auto.components.settings.artifacts.signIn', 'Sign in to Orca')}
</OrcaSignInButton>
<OrcaSignInWaitNotice className="basis-full" />
</section>
) : null}
</SettingsGroup>
<SettingsGroup
title={translate('auto.components.settings.artifacts.howToTitle', 'How to use Artifacts')}
description={
sharingEnabled
? translate(
'auto.components.settings.artifacts.howToDescription',
'Publish HTML or Markdown files as public links, then share them with your team.'
)
: translate(
'auto.components.settings.artifacts.howToDescriptionDisabled',
'Enable artifact sharing above to publish HTML or Markdown files as public links.'
)
}
bodyClassName="py-4"
>
<div className="space-y-4">
<ol className="space-y-3">
{howToSteps.map((step, index) => (
<li key={step.key} className="flex items-start gap-3">
<span className="flex size-6 shrink-0 items-center justify-center rounded-full border border-border/60 bg-muted/30 text-[11px] font-semibold text-muted-foreground">
{index + 1}
</span>
<div className="min-w-0 flex-1 space-y-0.5">
<p className="text-sm font-medium">{step.title}</p>
<p className="text-xs leading-relaxed text-muted-foreground">
{step.description}
</p>
</div>
</li>
))}
</ol>
<ol className="space-y-3">
{howToSteps.map((step, index) => (
<li key={step.key} className="flex items-start gap-3">
<span className="flex size-6 shrink-0 items-center justify-center rounded-full border border-border/60 bg-muted/30 text-[11px] font-semibold text-muted-foreground">
{index + 1}
<Button
type="button"
variant="ghost"
className="h-auto w-full justify-start whitespace-normal rounded-md border border-border/60 bg-muted/20 px-4 py-3 text-left hover:bg-muted/35 hover:text-foreground"
onClick={openArtifactsPage}
>
<span className="flex size-8 shrink-0 items-center justify-center rounded-md border border-border bg-background text-muted-foreground">
<Files className="size-4" />
</span>
<span className="min-w-0 flex-1 space-y-0.5">
<span className="block text-sm font-medium text-foreground">
{translate('auto.components.settings.artifacts.openArtifacts', 'Open Artifacts')}
</span>
<span className="block text-xs font-normal text-muted-foreground">
{translate(
'auto.components.settings.artifacts.openArtifactsDescriptionV2',
'Preview, copy, and manage links shared through your account.'
)}
</span>
<div className="min-w-0 flex-1 space-y-0.5">
<p className="text-sm font-medium">{step.title}</p>
<p className="text-xs leading-relaxed text-muted-foreground">{step.description}</p>
</div>
</li>
))}
</ol>
<Button
type="button"
variant="ghost"
className="h-auto w-full justify-start whitespace-normal rounded-md border border-border/60 bg-muted/20 px-4 py-3 text-left hover:bg-muted/35 hover:text-foreground"
onClick={openArtifactsPage}
>
<span className="flex size-8 shrink-0 items-center justify-center rounded-md border border-border bg-background text-muted-foreground">
<Files className="size-4" />
</span>
<span className="min-w-0 flex-1 space-y-0.5">
<span className="block text-sm font-medium text-foreground">
{translate('auto.components.settings.artifacts.openArtifacts', 'Open Artifacts')}
</span>
<span className="block text-xs font-normal text-muted-foreground">
{translate(
'auto.components.settings.artifacts.openArtifactsDescriptionV2',
'Preview, copy, and manage links shared through your account.'
)}
</span>
</span>
<ArrowRight className="ml-auto size-4 shrink-0 text-muted-foreground" />
</Button>
</section>
<ArrowRight className="ml-auto size-4 shrink-0 text-muted-foreground" />
</Button>
</div>
</SettingsGroup>
</div>
)
}
@@ -2,6 +2,7 @@ import { ArrowRight, CalendarClock } from 'lucide-react'
import type { GlobalSettings } from '../../../../../shared/global-settings-types'
import { Button } from '@/components/ui/button'
import { SettingsSwitchRow } from '../form-controls/SettingsFormControls'
import { SettingsGroup } from '../section-layout/SettingsGroup'
import { useAppStore } from '@/store/index'
import { translate } from '@/i18n/i18n'
@@ -15,125 +16,124 @@ export function AutomationsSettingsPane({
const openAutomationsPage = useAppStore((state) => state.openAutomationsPage)
return (
<div className="divide-y divide-border">
<SettingsSwitchRow
label={translate(
'auto.components.settings.automations.showButton',
'Show Automations Button'
<div className="space-y-8">
<SettingsGroup>
<SettingsSwitchRow
label={translate(
'auto.components.settings.automations.showButton',
'Show Automations Button'
)}
description={translate(
'auto.components.settings.automations.showButtonDescription',
'Show the Automations shortcut in the sidebar.'
)}
checked={settings.showAutomationsButton !== false}
onChange={() =>
void updateSettings({
showAutomationsButton: settings.showAutomationsButton === false
})
}
/>
</SettingsGroup>
<SettingsGroup
title={translate(
'auto.components.settings.automations.howItWorksTitle',
'How Automations work'
)}
description={translate(
'auto.components.settings.automations.showButtonDescription',
'Show the Automations shortcut in the sidebar.'
'auto.components.settings.automations.howItWorksDescription',
'Schedule agent work once, then let Orca create each run and keep its results together.'
)}
checked={settings.showAutomationsButton !== false}
onChange={() =>
void updateSettings({
showAutomationsButton: settings.showAutomationsButton === false
})
}
/>
<section className="space-y-4 py-5">
<div className="space-y-1">
<h3 className="text-sm font-medium">
{translate(
'auto.components.settings.automations.howItWorksTitle',
'How Automations work'
)}
</h3>
<p className="text-xs leading-relaxed text-muted-foreground">
{translate(
'auto.components.settings.automations.howItWorksDescription',
'Schedule agent work once, then let Orca create each run and keep its results together.'
)}
</p>
bodyClassName="py-4"
>
<div className="space-y-4">
<ol className="space-y-3">
<li className="flex items-start gap-3">
<span className="flex size-6 shrink-0 items-center justify-center rounded-full border border-border/60 bg-muted/30 text-[11px] font-semibold text-muted-foreground">
1
</span>
<div className="min-w-0 flex-1 space-y-0.5">
<p className="text-sm font-medium">
{translate(
'auto.components.settings.automations.defineStepTitle',
'Describe the work'
)}
</p>
<p className="text-xs leading-relaxed text-muted-foreground">
{translate(
'auto.components.settings.automations.defineStepDescription',
'Choose a project, agent, prompt, and schedule.'
)}
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex size-6 shrink-0 items-center justify-center rounded-full border border-border/60 bg-muted/30 text-[11px] font-semibold text-muted-foreground">
2
</span>
<div className="min-w-0 flex-1 space-y-0.5">
<p className="text-sm font-medium">
{translate(
'auto.components.settings.automations.runStepTitle',
'Orca starts each run'
)}
</p>
<p className="text-xs leading-relaxed text-muted-foreground">
{translate(
'auto.components.settings.automations.runStepDescription',
'The selected agent gets a fresh workspace when the schedule is due.'
)}
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex size-6 shrink-0 items-center justify-center rounded-full border border-border/60 bg-muted/30 text-[11px] font-semibold text-muted-foreground">
3
</span>
<div className="min-w-0 flex-1 space-y-0.5">
<p className="text-sm font-medium">
{translate(
'auto.components.settings.automations.reviewStepTitle',
'Review the results'
)}
</p>
<p className="text-xs leading-relaxed text-muted-foreground">
{translate(
'auto.components.settings.automations.reviewStepDescription',
'Inspect recent runs and continue the work whenever you need to.'
)}
</p>
</div>
</li>
</ol>
<Button
type="button"
variant="ghost"
className="h-auto w-full justify-start whitespace-normal rounded-md border border-border/60 bg-muted/20 px-4 py-3 text-left hover:bg-muted/35 hover:text-foreground"
onClick={openAutomationsPage}
>
<span className="flex size-8 shrink-0 items-center justify-center rounded-md border border-border bg-background text-muted-foreground">
<CalendarClock className="size-4" />
</span>
<span className="min-w-0 flex-1 space-y-0.5">
<span className="block text-sm font-medium text-foreground">
{translate(
'auto.components.settings.automations.openAutomations',
'Open Automations'
)}
</span>
<span className="block text-xs font-normal text-muted-foreground">
{translate(
'auto.components.settings.automations.openAutomationsDescription',
'Create schedules and inspect recent runs.'
)}
</span>
</span>
<ArrowRight className="ml-auto size-4 shrink-0 text-muted-foreground" />
</Button>
</div>
<ol className="space-y-3">
<li className="flex items-start gap-3">
<span className="flex size-6 shrink-0 items-center justify-center rounded-full border border-border/60 bg-muted/30 text-[11px] font-semibold text-muted-foreground">
1
</span>
<div className="min-w-0 flex-1 space-y-0.5">
<p className="text-sm font-medium">
{translate(
'auto.components.settings.automations.defineStepTitle',
'Describe the work'
)}
</p>
<p className="text-xs leading-relaxed text-muted-foreground">
{translate(
'auto.components.settings.automations.defineStepDescription',
'Choose a project, agent, prompt, and schedule.'
)}
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex size-6 shrink-0 items-center justify-center rounded-full border border-border/60 bg-muted/30 text-[11px] font-semibold text-muted-foreground">
2
</span>
<div className="min-w-0 flex-1 space-y-0.5">
<p className="text-sm font-medium">
{translate(
'auto.components.settings.automations.runStepTitle',
'Orca starts each run'
)}
</p>
<p className="text-xs leading-relaxed text-muted-foreground">
{translate(
'auto.components.settings.automations.runStepDescription',
'The selected agent gets a fresh workspace when the schedule is due.'
)}
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex size-6 shrink-0 items-center justify-center rounded-full border border-border/60 bg-muted/30 text-[11px] font-semibold text-muted-foreground">
3
</span>
<div className="min-w-0 flex-1 space-y-0.5">
<p className="text-sm font-medium">
{translate(
'auto.components.settings.automations.reviewStepTitle',
'Review the results'
)}
</p>
<p className="text-xs leading-relaxed text-muted-foreground">
{translate(
'auto.components.settings.automations.reviewStepDescription',
'Inspect recent runs and continue the work whenever you need to.'
)}
</p>
</div>
</li>
</ol>
<Button
type="button"
variant="ghost"
className="h-auto w-full justify-start whitespace-normal rounded-md border border-border/60 bg-muted/20 px-4 py-3 text-left hover:bg-muted/35 hover:text-foreground"
onClick={openAutomationsPage}
>
<span className="flex size-8 shrink-0 items-center justify-center rounded-md border border-border bg-background text-muted-foreground">
<CalendarClock className="size-4" />
</span>
<span className="min-w-0 flex-1 space-y-0.5">
<span className="block text-sm font-medium text-foreground">
{translate(
'auto.components.settings.automations.openAutomations',
'Open Automations'
)}
</span>
<span className="block text-xs font-normal text-muted-foreground">
{translate(
'auto.components.settings.automations.openAutomationsDescription',
'Create schedules and inspect recent runs.'
)}
</span>
</span>
<ArrowRight className="ml-auto size-4 shrink-0 text-muted-foreground" />
</Button>
</section>
</SettingsGroup>
</div>
)
}
@@ -30,7 +30,7 @@ export function BrowserDefaultZoomSetting({
"Zoom for sites you haven't zoomed."
)}
keywords={['browser', 'zoom', 'scale', 'default', 'page zoom', 'site', 'percentage']}
className="flex items-center justify-between gap-4 py-2"
className="flex items-center justify-between gap-4 py-3"
>
<div className="space-y-0.5">
<Label>
@@ -29,7 +29,7 @@ export function BrowserHomePageSetting({
'URL opened when creating a new browser tab. Leave empty to open a blank tab.'
)}
keywords={['browser', 'home', 'homepage', 'default', 'url', 'new tab', 'blank']}
className="flex items-start justify-between gap-4 py-2"
className="flex items-start justify-between gap-4 py-3"
>
<div className="min-w-0 shrink space-y-0.5">
<Label>
@@ -19,7 +19,7 @@ import { BrowserSshWorkspaceRoutingSetting } from './routing/BrowserSshWorkspace
import { BrowserProfileScopeSetting } from './profiles/BrowserProfileScopeSetting'
import { BrowserUserAgentSetting } from './BrowserUserAgentSetting'
import { BrowserSitePermissionsSetting } from './BrowserSitePermissionsSetting'
import { SettingsSubsectionHeader } from '../form-controls/SettingsFormControls'
import { SettingsGroup } from '../section-layout/SettingsGroup'
import { BrowserSessionCookiesSection } from './profiles/BrowserSessionCookiesSection'
import { BrowserNewProfileDialog } from './profiles/BrowserNewProfileDialog'
import { BrowserExtensionsSection } from './extensions/BrowserExtensionsSection'
@@ -230,65 +230,89 @@ export function BrowserPane({
})
}
const showPasswordsSection =
showPasswords &&
!isWebClientLocation() &&
selectedBrowserSessionHostId === LOCAL_EXECUTION_HOST_ID
const showExtensions =
settings.experimentalBrowserExtensions === true &&
selectedBrowserSessionHostId === LOCAL_EXECUTION_HOST_ID
return (
<div ref={setBrowserPaneRootNode} className="space-y-6">
<div ref={setBrowserPaneRootNode} className="space-y-8">
{showBrowserUse ? (
<BrowserUseSetup
onConfigureMoreBrowsers={scrollToSessionCookies}
onOpenComputerUse={onOpenComputerUse}
/>
<SettingsGroup>
<BrowserUseSetup
onConfigureMoreBrowsers={scrollToSessionCookies}
onOpenComputerUse={onOpenComputerUse}
/>
</SettingsGroup>
) : null}
{showHomePage ? (
<BrowserHomePageSetting
value={homePageDraft}
onChange={setHomePageDraft}
onSave={(url) => {
setBrowserDefaultUrl(url)
setHomePageDraftState(createBrowserHomePageDraftState(url ?? ''))
}}
/>
) : null}
{showHomePage ||
showSearchEngine ||
showDefaultZoom ||
showUserAgent ||
showSitePermissions ||
showLinkRouting ||
showLinkRoutingModifier ||
showLinkRoutingBackground ? (
<SettingsGroup>
{showHomePage ? (
<BrowserHomePageSetting
value={homePageDraft}
onChange={setHomePageDraft}
onSave={(url) => {
setBrowserDefaultUrl(url)
setHomePageDraftState(createBrowserHomePageDraftState(url ?? ''))
}}
/>
) : null}
{showSearchEngine ? (
<BrowserSearchEngineSetting
selectedSearchEngine={selectedSearchEngine}
onSearchEngineChange={(engine) => {
setBrowserDefaultSearchEngine(engine === 'google' ? null : engine)
}}
/>
) : null}
{showSearchEngine ? (
<BrowserSearchEngineSetting
selectedSearchEngine={selectedSearchEngine}
onSearchEngineChange={(engine) => {
setBrowserDefaultSearchEngine(engine === 'google' ? null : engine)
}}
/>
) : null}
{showDefaultZoom ? (
<BrowserDefaultZoomSetting
value={browserDefaultZoomLevel}
onChange={setBrowserDefaultZoomLevel}
/>
) : null}
{showDefaultZoom ? (
<BrowserDefaultZoomSetting
value={browserDefaultZoomLevel}
onChange={setBrowserDefaultZoomLevel}
/>
) : null}
{showUserAgent ? <BrowserUserAgentSetting hostId={settingsFocusedHostId} /> : null}
{showUserAgent ? <BrowserUserAgentSetting hostId={settingsFocusedHostId} /> : null}
{showSitePermissions ? <BrowserSitePermissionsSetting /> : null}
{showSitePermissions ? <BrowserSitePermissionsSetting /> : null}
{showLinkRouting ? (
<BrowserLinkRoutingSetting
settings={settings}
linkRoutingDescription={linkRoutingDescription}
isMac={isMac}
updateSettings={updateSettings}
/>
) : null}
{showLinkRouting ? (
<BrowserLinkRoutingSetting
settings={settings}
linkRoutingDescription={linkRoutingDescription}
isMac={isMac}
updateSettings={updateSettings}
/>
) : null}
{showLinkRoutingModifier ? (
<BrowserLinkRoutingModifierSetting
settings={settings}
isMac={isMac}
updateSettings={updateSettings}
/>
) : null}
{showLinkRoutingModifier ? (
<BrowserLinkRoutingModifierSetting
settings={settings}
isMac={isMac}
updateSettings={updateSettings}
/>
) : null}
{showLinkRoutingBackground ? (
<BrowserLinkRoutingBackgroundSetting settings={settings} updateSettings={updateSettings} />
{showLinkRoutingBackground ? (
<BrowserLinkRoutingBackgroundSetting
settings={settings}
updateSettings={updateSettings}
/>
) : null}
</SettingsGroup>
) : null}
{showTerminalLinkActions ? (
@@ -299,71 +323,78 @@ export function BrowserPane({
/>
) : null}
{showLocalhostLabels ? (
<BrowserLocalhostWorktreeLabelsSetting
settings={settings}
updateSettings={updateSettings}
/>
) : null}
{showLocalhostLabels || showProfileScope ? (
<SettingsGroup>
{showLocalhostLabels ? (
<BrowserLocalhostWorktreeLabelsSetting
settings={settings}
updateSettings={updateSettings}
/>
) : null}
{showProfileScope ? (
<BrowserProfileScopeSetting settings={settings} updateSettings={updateSettings} />
{showProfileScope ? (
<BrowserProfileScopeSetting settings={settings} updateSettings={updateSettings} />
) : null}
</SettingsGroup>
) : null}
{showClientHostedRemote || showSshWorkspaceRouting ? (
<SettingsSubsectionHeader
className="pt-2"
<SettingsGroup
title={translate('settings.browser.remoteBrowsing.heading', 'Remote browsing')}
description={translate(
'settings.browser.remoteBrowsing.headingDescription',
'Where remote workspace pages render, and where their network traffic leaves from.'
)}
/>
>
{showClientHostedRemote ? (
<BrowserClientHostedRemoteSetting settings={settings} updateSettings={updateSettings} />
) : null}
{showSshWorkspaceRouting ? (
<BrowserSshWorkspaceRoutingSetting
settings={settings}
updateSettings={updateSettings}
/>
) : null}
</SettingsGroup>
) : null}
{showClientHostedRemote ? (
<BrowserClientHostedRemoteSetting settings={settings} updateSettings={updateSettings} />
) : null}
{showCookies || showPasswordsSection || showExtensions ? (
<SettingsGroup>
{showCookies ? (
<BrowserSessionCookiesSection
defaultProfile={defaultProfile}
nonDefaultProfiles={nonDefaultProfiles}
detectedBrowsers={detectedBrowsers}
importState={browserSessionImportState}
hostOptions={browserSessionHostOptions}
selectedHostId={selectedBrowserSessionHostId}
activeProfileId={activeBrowserSessionProfileId}
onAddProfile={() => setNewProfileDialogOpen(true)}
onSelectHost={selectBrowserSessionHost}
onSelectDefaultProfile={() => setDefaultBrowserSessionProfileId(null)}
onSelectProfile={setDefaultBrowserSessionProfileId}
/>
) : null}
{showSshWorkspaceRouting ? (
<BrowserSshWorkspaceRoutingSetting settings={settings} updateSettings={updateSettings} />
) : null}
{/* The vault lives on this computer only, so it follows the local host's profiles. */}
{showPasswordsSection ? (
<BrowserPasswordsSection
browserProfileId={activeBrowserSessionProfileId}
profileLabel={activeBrowserSessionProfileLabel}
settings={settings}
updateSettings={updateSettings}
/>
) : null}
{showCookies ? (
<BrowserSessionCookiesSection
defaultProfile={defaultProfile}
nonDefaultProfiles={nonDefaultProfiles}
detectedBrowsers={detectedBrowsers}
importState={browserSessionImportState}
hostOptions={browserSessionHostOptions}
selectedHostId={selectedBrowserSessionHostId}
activeProfileId={activeBrowserSessionProfileId}
onAddProfile={() => setNewProfileDialogOpen(true)}
onSelectHost={selectBrowserSessionHost}
onSelectDefaultProfile={() => setDefaultBrowserSessionProfileId(null)}
onSelectProfile={setDefaultBrowserSessionProfileId}
/>
) : null}
{/* The vault lives on this computer only, so it follows the local host's profiles. */}
{showPasswords &&
!isWebClientLocation() &&
selectedBrowserSessionHostId === LOCAL_EXECUTION_HOST_ID ? (
<BrowserPasswordsSection
browserProfileId={activeBrowserSessionProfileId}
profileLabel={activeBrowserSessionProfileLabel}
settings={settings}
updateSettings={updateSettings}
/>
) : null}
{/* Why local only: release 1 runs extensions in this computer's browser profiles. */}
{settings.experimentalBrowserExtensions === true &&
selectedBrowserSessionHostId === LOCAL_EXECUTION_HOST_ID ? (
<BrowserExtensionsSection
profileId={activeBrowserSessionProfileId}
profileLabel={activeBrowserSessionProfileLabel}
/>
{/* Why local only: release 1 runs extensions in this computer's browser profiles. */}
{showExtensions ? (
<BrowserExtensionsSection
profileId={activeBrowserSessionProfileId}
profileLabel={activeBrowserSessionProfileLabel}
/>
) : null}
</SettingsGroup>
) : null}
<BrowserNewProfileDialog open={newProfileDialogOpen} onOpenChange={setNewProfileDialogOpen} />
@@ -34,7 +34,7 @@ export function BrowserSearchEngineSetting({
'token',
'omnibox'
]}
className="flex items-start justify-between gap-4 py-2"
className="flex items-start justify-between gap-4 py-3"
>
<div className="space-y-0.5">
<Label>
@@ -118,7 +118,7 @@ export function BrowserSitePermissionsSetting(): React.JSX.Element {
}
return (
<SearchableSetting id="browser-site-permissions" {...searchEntry} className="space-y-3 py-2">
<SearchableSetting id="browser-site-permissions" {...searchEntry} className="space-y-3 py-3">
<div className="flex items-center justify-between gap-3">
<div className="space-y-0.5">
<Label>{searchEntry.title}</Label>
@@ -22,7 +22,7 @@ export function BrowserExtensionsExperimentalSetting({
title={entry.title}
description={entry.description}
keywords={entry.keywords}
className="space-y-3 py-2"
className="space-y-3 py-3"
id="experimental-browser-extensions"
>
<div className="flex items-start justify-between gap-4">
@@ -220,7 +220,7 @@ export function BrowserExtensionsSection({
title={entry.title}
description={entry.description}
keywords={entry.keywords}
className="space-y-3 py-2"
className="space-y-3 py-3"
>
<div className="flex items-center justify-between gap-3">
<div className="min-w-0 space-y-0.5">
@@ -60,7 +60,7 @@ export function BrowserPasswordsSection(props: BrowserPasswordsSectionProps): Re
title={entry.title}
description={entry.description}
keywords={entry.keywords}
className="space-y-3 py-2"
className="space-y-3 py-3"
>
<BrowserPasswordsProfile key={props.browserProfileId} {...props} />
</SearchableSetting>
@@ -73,7 +73,7 @@ export function BrowserSessionCookiesSection({
'arc',
'profile'
]}
className="space-y-3 py-2"
className="space-y-3 py-3"
>
<div className="flex items-center justify-between gap-3">
<div className="space-y-0.5">
@@ -32,7 +32,7 @@ export function BrowserLinkRoutingSetting({
'file',
'editor'
]}
className="flex items-center justify-between gap-4 py-2"
className="flex items-center justify-between gap-4 py-3"
>
<div className="space-y-0.5">
<Label>
@@ -2,16 +2,13 @@ import type { GlobalSettings } from '../../../../../../shared/global-settings-ty
import { translate } from '@/i18n/i18n'
import { BROWSER_TERMINAL_LINK_ACTIONS_SETTINGS_TARGET_ID } from '@/lib/settings-navigation-types'
import { SearchableSetting } from '../../section-layout/SearchableSetting'
import {
SettingsRow,
SettingsSegmentedControl,
SettingsSubsectionHeader
} from '../../form-controls/SettingsFormControls'
import { SettingsRow, SettingsSegmentedControl } from '../../form-controls/SettingsFormControls'
import { getTerminalLinkActionSearchKeywords } from '../browser-search'
import {
terminalLinkClickBehaviorFor,
type TerminalLinkClickBehavior
} from '../../../terminal-pane/links/gestures/terminal-link-click-behavior'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
type BrowserTerminalLinkActionsSettingProps = {
settings: Pick<
@@ -82,48 +79,43 @@ export function BrowserTerminalLinkActionsSetting({
description={description}
keywords={getTerminalLinkActionSearchKeywords({ isMac })}
>
<section className="space-y-3">
<SettingsSubsectionHeader title={title} description={description} />
<div className="rounded-lg border border-border/60 bg-muted/10 px-4">
<div className="divide-y divide-border/40">
<SettingsRow
label={plainClickLabel}
description={plainClickDescription}
alignTop
control={
<SettingsSegmentedControl<TerminalLinkClickBehavior>
value={behavior}
onChange={(value) => updateSettings({ terminalLinkClickBehavior: value })}
ariaLabel={plainClickAriaLabel}
size="sm"
options={[
{ value: 'actions', label: actionsLabel },
{ value: 'open', label: openUrlLabel },
{ value: 'none', label: leaveToTerminalLabel }
]}
/>
}
<SettingsGroup title={title} description={description}>
<SettingsRow
label={plainClickLabel}
description={plainClickDescription}
alignTop
control={
<SettingsSegmentedControl<TerminalLinkClickBehavior>
value={behavior}
onChange={(value) => updateSettings({ terminalLinkClickBehavior: value })}
ariaLabel={plainClickAriaLabel}
size="sm"
options={[
{ value: 'actions', label: actionsLabel },
{ value: 'open', label: openUrlLabel },
{ value: 'none', label: leaveToTerminalLabel }
]}
/>
<SettingsRow
label={middleClickLabel}
description={middleClickDescription}
control={
<SettingsSegmentedControl<TerminalLinkClickBehavior>
value={settings.terminalUrlMiddleClickBehavior ?? 'open'}
onChange={(value) => updateSettings({ terminalUrlMiddleClickBehavior: value })}
ariaLabel={middleClickAriaLabel}
size="sm"
options={[
{ value: 'actions', label: actionsLabel },
{ value: 'open', label: openUrlLabel },
{ value: 'none', label: leaveToTerminalLabel }
]}
/>
}
}
/>
<SettingsRow
label={middleClickLabel}
description={middleClickDescription}
control={
<SettingsSegmentedControl<TerminalLinkClickBehavior>
value={settings.terminalUrlMiddleClickBehavior ?? 'open'}
onChange={(value) => updateSettings({ terminalUrlMiddleClickBehavior: value })}
ariaLabel={middleClickAriaLabel}
size="sm"
options={[
{ value: 'actions', label: actionsLabel },
{ value: 'open', label: openUrlLabel },
{ value: 'none', label: leaveToTerminalLabel }
]}
/>
</div>
</div>
</section>
}
/>
</SettingsGroup>
</SearchableSetting>
)
}
@@ -86,7 +86,7 @@ export function BrowserUseSetup({
if (!browserUseEnabled) {
return (
<div className="flex items-center justify-between gap-4 py-2">
<div className="flex items-center justify-between gap-4 py-3">
<div className="space-y-0.5">
<p className="text-sm font-medium">
{translate('auto.components.settings.BrowserUsePane.b8a1f2d84d', 'Agent Browser Use')}
@@ -107,7 +107,7 @@ export function BrowserUseSetup({
}
return (
<div className="space-y-3 rounded-2xl border border-border/60 bg-card/30 p-4">
<div className="space-y-3 py-4">
<div className="flex items-center justify-between gap-3">
<div className="space-y-0.5">
<p className="text-sm font-semibold">
@@ -21,6 +21,7 @@ import {
getSkillDiscoveryTargetForRuntime
} from '../../../../lib/skill-runtime-command'
import { WslCliRegistration } from './WslCliRegistration'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
import { useCliRegistrationActions } from './use-cli-registration-actions'
import { useLocalCliSkillFreshnessName } from '../../../../hooks/use-local-cli-skill-freshness-name'
import { translate } from '@/i18n/i18n'
@@ -149,157 +150,155 @@ export function CliSection({
const canRevealCommandPath =
status?.commandPath != null && ['installed', 'stale', 'conflict'].includes(status.state)
const shellCommandLabel = translate(
'auto.components.settings.CliSection.38edbb5721',
'Shell command'
)
const agentSkillsLabel = translate(
'auto.components.settings.CliSection.04873eea3e',
'Agent skills'
)
return (
<section className="space-y-4" data-settings-section="cli">
<div className="space-y-1">
<h2 className="text-sm font-semibold">
{translate('auto.components.settings.CliSection.c5c0f2641d', 'Orca CLI')}
</h2>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.CliSection.outsideOrcaDescription',
'Orca terminals already have `orca`. Turn this on to use `orca` from other terminals outside Orca.'
)}
</p>
</div>
<div data-settings-section="cli">
<SettingsGroup
title={translate('auto.components.settings.CliSection.c5c0f2641d', 'Orca CLI')}
description={translate(
'auto.components.settings.CliSection.outsideOrcaDescription',
'Orca terminals already have `orca`. Turn this on to use `orca` from other terminals outside Orca.'
)}
>
<div data-settings-row="" className="space-y-3 py-3">
<div className="flex items-center justify-between gap-4">
<div className="space-y-0.5">
<Label>{shellCommandLabel}</Label>
<p
className={`text-xs ${pathStatusUnknown ? 'text-amber-600 dark:text-amber-400' : 'text-muted-foreground'}`}
>
{loading
? translate(
'auto.components.settings.CliSection.d363e5929b',
'Checking CLI registration…'
)
: (status?.detail ?? getInstallDescription(currentPlatform))}
</p>
</div>
<div className="flex items-center gap-2">
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-xs"
onClick={() => void refreshStatus()}
disabled={loading || busyAction !== null}
aria-label={translate(
'auto.components.settings.CliSection.52e640f3a0',
'Refresh CLI status'
)}
>
<RefreshCw className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate('auto.components.settings.CliSection.5dae812f50', 'Refresh')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
{!isBrowserManaged ? (
<Switch
aria-label={shellCommandLabel}
checked={isEnabled}
disabled={loading || !isSupported || pathStatusUnknown || busyAction !== null}
onCheckedChange={() => setDialogOpen(true)}
className="disabled:opacity-60"
/>
) : null}
</div>
</div>
<div className="space-y-3 rounded-xl border border-border/60 bg-card/50 p-4">
<div className="flex items-center justify-between gap-4">
<div className="space-y-0.5">
<Label>
{translate('auto.components.settings.CliSection.38edbb5721', 'Shell command')}
</Label>
<p
className={`text-xs ${pathStatusUnknown ? 'text-amber-600 dark:text-amber-400' : 'text-muted-foreground'}`}
>
{loading
? translate(
'auto.components.settings.CliSection.d363e5929b',
'Checking CLI registration…'
)
: (status?.detail ?? getInstallDescription(currentPlatform))}
{status?.commandPath ? (
<p className="text-xs text-muted-foreground">
{translate('auto.components.settings.CliSection.15eaad0d31', 'Command path:')}{' '}
<code className="rounded bg-muted px-1 py-0.5 text-[11px]">{status.commandPath}</code>
</p>
</div>
<div className="flex items-center gap-2">
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-xs"
onClick={() => void refreshStatus()}
disabled={loading || busyAction !== null}
aria-label={translate(
'auto.components.settings.CliSection.52e640f3a0',
'Refresh CLI status'
)}
>
<RefreshCw className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate('auto.components.settings.CliSection.5dae812f50', 'Refresh')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
{!isBrowserManaged ? (
<Switch
aria-label={translate(
'auto.components.settings.CliSection.38edbb5721',
'Shell command'
)}
checked={isEnabled}
disabled={loading || !isSupported || pathStatusUnknown || busyAction !== null}
onCheckedChange={() => setDialogOpen(true)}
className="disabled:opacity-60"
/>
) : null}
</div>
</div>
) : null}
{status?.commandPath ? (
<p className="text-xs text-muted-foreground">
{translate('auto.components.settings.CliSection.15eaad0d31', 'Command path:')}{' '}
<code className="rounded bg-muted px-1 py-0.5 text-[11px]">{status.commandPath}</code>
</p>
) : null}
{status?.state === 'stale' && status.currentTarget ? (
<p className="text-xs text-amber-600 dark:text-amber-400">
{translate(
'auto.components.settings.CliSection.b0c310ab46',
'Existing launcher target:'
)}{' '}
<code>{status.currentTarget}</code>
</p>
) : null}
{status?.state === 'installed' &&
status.pathConfigured === false &&
status.pathDirectory ? (
<p className="text-xs text-amber-600 dark:text-amber-400">
{status.pathDirectory}{' '}
{translate(
'auto.components.settings.CliSection.7f2747f7dd',
'is not currently visible on PATH for this shell.'
)}
</p>
) : null}
{!loading && !isSupported && !isBrowserManaged && status?.detail ? (
<p className="text-xs text-muted-foreground">{status.detail}</p>
) : null}
{installFailure ? (
<div
role="alert"
className="space-y-1 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive"
>
<p className="font-medium">
{status?.state === 'stale' && status.currentTarget ? (
<p className="text-xs text-amber-600 dark:text-amber-400">
{translate(
'auto.components.settings.CliSection.a2b13efa94',
'Failed to register `{{value0}}` in PATH.',
{ value0: commandName }
'auto.components.settings.CliSection.b0c310ab46',
'Existing launcher target:'
)}{' '}
<code>{status.currentTarget}</code>
</p>
) : null}
{status?.state === 'installed' &&
status.pathConfigured === false &&
status.pathDirectory ? (
<p className="text-xs text-amber-600 dark:text-amber-400">
{status.pathDirectory}{' '}
{translate(
'auto.components.settings.CliSection.7f2747f7dd',
'is not currently visible on PATH for this shell.'
)}
</p>
<p className="leading-snug">{installFailure.reason}</p>
{installFailure.conflictCommandPath ? (
<p className="leading-snug">
) : null}
{!loading && !isSupported && !isBrowserManaged && status?.detail ? (
<p className="text-xs text-muted-foreground">{status.detail}</p>
) : null}
{installFailure ? (
<div
role="alert"
className="space-y-1 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive"
>
<p className="font-medium">
{translate(
'auto.components.settings.CliSection.installFailureConflictRemedy',
'Remove {{value0}} and register again if it is no longer needed.',
{ value0: installFailure.conflictCommandPath }
'auto.components.settings.CliSection.a2b13efa94',
'Failed to register `{{value0}}` in PATH.',
{ value0: commandName }
)}
</p>
) : null}
</div>
) : null}
<p className="leading-snug">{installFailure.reason}</p>
{installFailure.conflictCommandPath ? (
<p className="leading-snug">
{translate(
'auto.components.settings.CliSection.installFailureConflictRemedy',
'Remove {{value0}} and register again if it is no longer needed.',
{ value0: installFailure.conflictCommandPath }
)}
</p>
) : null}
</div>
) : null}
<div className="flex items-center gap-2">
{status?.commandPath ? (
<Button
variant="ghost"
size="sm"
onClick={() => {
if (status.commandPath) {
void window.api.shell.openPath(status.commandPath, LOCAL_EXECUTION_HOST_ID)
}
}}
disabled={loading || !canRevealCommandPath}
className="gap-2"
>
<FolderOpen className="size-3.5" />
{revealLabel}
</Button>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => {
if (status.commandPath) {
void window.api.shell.openPath(status.commandPath, LOCAL_EXECUTION_HOST_ID)
}
}}
disabled={loading || !canRevealCommandPath}
className="gap-2"
>
<FolderOpen className="size-3.5" />
{revealLabel}
</Button>
</div>
) : null}
</div>
{!isBrowserManaged ? (
<div className="border-t border-border/60 pt-3">
<div data-settings-row="" className="py-3">
<div className="space-y-0.5">
<Label>
{translate('auto.components.settings.CliSection.04873eea3e', 'Agent skills')}
</Label>
<Label>{agentSkillsLabel}</Label>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.CliSection.36a6f919ba',
@@ -328,9 +327,9 @@ export function CliSection({
/>
</div>
) : null}
</div>
<WslCliRegistration currentPlatform={currentPlatform} />
<WslCliRegistration currentPlatform={currentPlatform} />
</SettingsGroup>
<CliRegistrationDialog
busyAction={busyAction}
@@ -343,6 +342,6 @@ export function CliSection({
onRemove={remove}
open={dialogOpen}
/>
</section>
</div>
)
}
@@ -148,17 +148,17 @@ export function WslCliRegistration({
}
}
const wslShellCommandLabel = translate(
'auto.components.settings.WslCliRegistration.d9c6880dbd',
'WSL shell command'
)
return (
<>
<div className="space-y-3 rounded-xl border border-border/60 bg-card/50 p-4">
<div data-settings-row="" className="space-y-3 py-3">
<div className="flex items-center justify-between gap-4">
<div className="space-y-0.5">
<Label>
{translate(
'auto.components.settings.WslCliRegistration.d9c6880dbd',
'WSL shell command'
)}
</Label>
<Label>{wslShellCommandLabel}</Label>
<p className="text-xs text-muted-foreground">
{loading
? translate(
@@ -10,6 +10,7 @@ import { useAppStore } from '@/store/index'
import { Button } from '../../ui/button'
import { Badge } from '../../ui/badge'
import { ComputerUseSkillSetupPanel } from './ComputerUseSkillSetupPanel'
import { SettingsGroup } from '../section-layout/SettingsGroup'
import { translate } from '@/i18n/i18n'
type PermissionDefinition = {
@@ -290,10 +291,10 @@ export function ComputerUsePane(): React.JSX.Element {
const isMac = platform === null || platform === 'darwin'
return (
<div className="space-y-5">
<div className="space-y-8">
{isMac ? (
<>
<div className="flex flex-wrap items-start justify-between gap-4 rounded-lg border border-border/60 bg-muted/25 px-4 py-3">
<SettingsGroup>
<div className="flex flex-wrap items-start justify-between gap-4 py-4">
<div className="space-y-1">
<div className="flex items-center gap-2 text-sm font-medium">
<ShieldCheck className="size-4" />
@@ -321,76 +322,77 @@ export function ComputerUsePane(): React.JSX.Element {
</Button>
</div>
<div className="space-y-2">
<div className="divide-y divide-border/60 rounded-lg border border-border/60">
{PERMISSIONS.map((permission) => {
const status = stateById.get(permission.id)
const pending = pendingId === permission.id
{PERMISSIONS.map((permission) => {
const status = stateById.get(permission.id)
const pending = pendingId === permission.id
return (
<div
key={permission.id}
className="flex items-center justify-between gap-4 px-4 py-3"
>
<div className="flex min-w-0 items-start gap-3">
<div className="mt-0.5 text-muted-foreground">{permission.icon}</div>
<div className="min-w-0 space-y-1">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-medium">
{translate(permission.labelKey, permission.labelDefault)}
</span>
<span
className={`rounded-full border px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider ${statusClass(
status
)}`}
>
{statusLabel(status)}
</span>
</div>
<p className="text-xs text-muted-foreground">
{translate(permission.descriptionKey, permission.descriptionDefault)}
</p>
</div>
</div>
<div className="flex w-28 shrink-0 justify-end">
<Button
variant="outline"
size="sm"
disabled={
resetting ||
pending ||
status === 'unsupported' ||
helperUnavailableReason !== null
}
onClick={() => void openPermission(permission.id)}
className="gap-1.5"
return (
<div
key={permission.id}
data-settings-row=""
className="flex items-center justify-between gap-4 py-3"
>
<div className="flex min-w-0 items-start gap-3">
<div className="mt-0.5 text-muted-foreground">{permission.icon}</div>
<div className="min-w-0 space-y-1">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-medium">
{translate(permission.labelKey, permission.labelDefault)}
</span>
<span
className={`rounded-full border px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider ${statusClass(
status
)}`}
>
<ExternalLink className="size-3.5" />
{translate('auto.components.settings.ComputerUsePane.45f8e22c2e', 'Open')}
</Button>
{statusLabel(status)}
</span>
</div>
<p className="text-xs text-muted-foreground">
{translate(permission.descriptionKey, permission.descriptionDefault)}
</p>
</div>
</div>
<div className="flex w-28 shrink-0 justify-end">
<Button
variant="outline"
size="sm"
disabled={
resetting ||
pending ||
status === 'unsupported' ||
helperUnavailableReason !== null
}
onClick={() => void openPermission(permission.id)}
className="gap-1.5"
>
<ExternalLink className="size-3.5" />
{translate('auto.components.settings.ComputerUsePane.45f8e22c2e', 'Open')}
</Button>
</div>
</div>
)
})}
<button
type="button"
disabled={resetAccessDisabled}
onClick={() => void resetAccess()}
className="mb-3 ml-auto block w-28 text-right text-xs text-muted-foreground underline underline-offset-2 hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
>
{resetting
? translate(
'auto.components.settings.ComputerUsePane.506f2acf7a',
'Resetting access...'
)
})}
</div>
<button
type="button"
disabled={resetAccessDisabled}
onClick={() => void resetAccess()}
className="ml-auto mr-4 block w-28 text-right text-xs text-muted-foreground underline underline-offset-2 hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
>
{resetting
? translate(
'auto.components.settings.ComputerUsePane.506f2acf7a',
'Resetting access...'
)
: translate('auto.components.settings.ComputerUsePane.6b17602073', 'Reset access')}
</button>
</div>
</>
: translate('auto.components.settings.ComputerUsePane.6b17602073', 'Reset access')}
</button>
</SettingsGroup>
) : null}
<ComputerUseSkillSetupPanel />
<SettingsGroup>
<div className="py-4">
<ComputerUseSkillSetupPanel />
</div>
</SettingsGroup>
</div>
)
}
@@ -23,6 +23,7 @@ export function ComputerUseSkillSetupPanel(): React.JSX.Element {
return (
<AgentSkillSetupPanel
variant="inline"
title={translate(
'auto.components.settings.ComputerUsePane.93255aaf18',
'Orca Computer Use skill'
@@ -27,6 +27,7 @@ import {
} from './developer-permission-status'
import { showDeveloperPermissionRequestNotice } from './developer-permission-request-notice'
import { TerminalTccAttributionNotice } from './TerminalTccAttributionNotice'
import { SettingsGroup } from '../section-layout/SettingsGroup'
type DeveloperPermissionsPaneProps = {
highlightedSettingId?: string | null
@@ -311,31 +312,29 @@ export function DeveloperPermissionsPane({
}
return (
<div className="space-y-5">
<div className="space-y-8">
<TerminalTccAttributionNotice />
<div className="flex items-start justify-between gap-4 rounded-lg border border-border/60 bg-muted/25 px-4 py-3">
<div className="space-y-1">
<div className="flex items-center gap-2 text-sm font-medium">
<SettingsGroup
title={
<span className="flex items-center gap-2">
<ShieldCheck className="size-4" />
{translate(
'auto.components.settings.DeveloperPermissionsPane.6f011b9bf6',
"Terminal tools inherit Orca's macOS privacy envelope."
)}
</div>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.DeveloperPermissionsPane.6326a4c5cc',
'Use these controls when a CLI, local app, or automation tool needs macOS privacy access. Orca does not ask at startup.'
)}
</p>
</div>
<Button variant="outline" size="sm" className="gap-1.5" onClick={() => void refresh()}>
<RefreshCw className={`size-3.5 ${loading ? 'animate-spin' : ''}`} />
{translate('auto.components.settings.DeveloperPermissionsPane.4c17304beb', 'Refresh')}
</Button>
</div>
<div className="divide-y divide-border/60 rounded-lg border border-border/60">
</span>
}
description={translate(
'auto.components.settings.DeveloperPermissionsPane.6326a4c5cc',
'Use these controls when a CLI, local app, or automation tool needs macOS privacy access. Orca does not ask at startup.'
)}
action={
<Button variant="outline" size="sm" className="gap-1.5" onClick={() => void refresh()}>
<RefreshCw className={`size-3.5 ${loading ? 'animate-spin' : ''}`} />
{translate('auto.components.settings.DeveloperPermissionsPane.4c17304beb', 'Refresh')}
</Button>
}
>
{PERMISSIONS.map((permission) => {
const state = stateById.get(permission.id)
const status = state?.status
@@ -343,11 +342,11 @@ export function DeveloperPermissionsPane({
const settingId = `developer-permissions-${permission.id}`
return (
<div key={permission.id}>
<div key={permission.id} data-settings-row="">
<div
data-settings-section={settingId}
data-highlighted={highlightedSettingId === settingId ? 'true' : undefined}
className="flex items-center justify-between gap-4 px-4 py-3 transition-[background-color,box-shadow] duration-500 data-[highlighted=true]:bg-accent data-[highlighted=true]:ring-2 data-[highlighted=true]:ring-inset data-[highlighted=true]:ring-ring/50 motion-reduce:transition-none"
className="-mx-5 flex items-center justify-between gap-4 px-5 py-3 transition-[background-color,box-shadow] duration-500 data-[highlighted=true]:bg-accent data-[highlighted=true]:ring-2 data-[highlighted=true]:ring-inset data-[highlighted=true]:ring-ring/50 motion-reduce:transition-none"
>
<div className="flex min-w-0 items-start gap-3">
<div className="mt-0.5 text-muted-foreground">{permission.icon}</div>
@@ -400,7 +399,7 @@ export function DeveloperPermissionsPane({
</div>
)
})}
</div>
</SettingsGroup>
</div>
)
}
@@ -98,7 +98,7 @@ export function LocalNetworkConnectionTest(): React.JSX.Element {
}
return (
<Collapsible open={open} onOpenChange={setOpen} className="mr-4 mb-3 ml-11">
<Collapsible open={open} onOpenChange={setOpen} className="mb-3 ml-7">
<CollapsibleTrigger asChild>
<Button
type="button"
@@ -43,7 +43,7 @@ export function CollapseUnchangedRegionsSetting({
title={title}
description={description}
keywords={KEYWORDS}
className="flex items-center justify-between gap-4 py-2"
className="flex items-center justify-between gap-4 py-3"
>
<div className="min-w-0 flex-1 space-y-0.5">
<Label>{title}</Label>
@@ -24,7 +24,7 @@ export function DiffShowWhitespaceSetting({
'Show leading and trailing whitespace differences in diffs.'
)}
keywords={['diff', 'whitespace', 'spaces', 'tabs', 'trim', 'indentation']}
className="flex items-center justify-between gap-4 py-2"
className="flex items-center justify-between gap-4 py-3"
>
<div className="min-w-0 flex-1 space-y-0.5">
<Label>
@@ -24,7 +24,7 @@ export function EditorWordWrapSetting({
'Wrap long lines in file editors instead of requiring horizontal scrolling.'
)}
keywords={['editor', 'code', 'word wrap', 'wrap', 'horizontal scroll', 'long lines']}
className="flex items-center justify-between gap-4 py-2"
className="flex items-center justify-between gap-4 py-3"
>
<div className="min-w-0 flex-1 space-y-0.5">
<Label>
@@ -11,11 +11,7 @@ import { Input } from '../../ui/input'
import { Label } from '../../ui/label'
import { SearchableSetting } from '../section-layout/SearchableSetting'
import { CollapseUnchangedRegionsSetting } from './CollapseUnchangedRegionsSetting'
import {
SettingsSegmentedControl,
SettingsSubsectionHeader,
SettingsSwitchRow
} from '../form-controls/SettingsFormControls'
import { SettingsSegmentedControl, SettingsSwitchRow } from '../form-controls/SettingsFormControls'
import { translate } from '@/i18n/i18n'
import { RichMarkdownSpellcheckSetting } from './RichMarkdownSpellcheckSetting'
import { MarkdownDefaultViewSetting } from './MarkdownDefaultViewSetting'
@@ -30,6 +26,7 @@ import {
resolveAutoSaveDelayDraftState,
updateAutoSaveDelayDraftState
} from './auto-save-delay-draft'
import { SettingsGroup } from '../section-layout/SettingsGroup'
type GeneralEditorSettingsSectionProps = {
settings: GlobalSettings
@@ -89,18 +86,17 @@ export function GeneralEditorSettingsSection({
}
return (
<section key="editor" className="space-y-4">
<SettingsSubsectionHeader
title={translate(
'auto.components.settings.GeneralEditorSettingsSection.45c6e85c4d',
'Editor'
)}
description={translate(
'auto.components.settings.GeneralEditorSettingsSection.d21136d9ef',
'Configure how Orca persists file edits.'
)}
/>
<SettingsGroup
key="editor"
title={translate(
'auto.components.settings.GeneralEditorSettingsSection.45c6e85c4d',
'Editor'
)}
description={translate(
'auto.components.settings.GeneralEditorSettingsSection.d21136d9ef',
'Configure how Orca persists file edits.'
)}
>
<EditorFormatOnSaveSetting settings={settings} updateSettings={updateSettings} />
<EditorInsertFinalNewlineSetting settings={settings} updateSettings={updateSettings} />
@@ -139,7 +135,7 @@ export function GeneralEditorSettingsSection({
'How long Orca waits after your last edit before saving automatically.'
)}
keywords={['autosave', 'delay', 'milliseconds']}
className="flex items-center justify-between gap-4 py-2"
className="flex items-center justify-between gap-4 py-3"
>
<div className="min-w-0 flex-1 space-y-0.5">
<Label>
@@ -193,7 +189,7 @@ export function GeneralEditorSettingsSection({
'Preferred presentation format for showing git diffs by default.'
)}
keywords={['diff', 'view', 'inline', 'side-by-side', 'split']}
className="flex items-center justify-between gap-4 py-2"
className="flex items-center justify-between gap-4 py-3"
>
<div className="min-w-0 flex-1 space-y-0.5">
<Label>
@@ -260,7 +256,7 @@ export function GeneralEditorSettingsSection({
'Wrap long lines in diff editors instead of requiring horizontal scrolling.'
)}
keywords={['diff', 'word wrap', 'wrap', 'markdown', 'long lines']}
className="flex items-center justify-between gap-4 py-2"
className="flex items-center justify-between gap-4 py-3"
>
<div className="min-w-0 flex-1 space-y-0.5">
<Label>
@@ -312,7 +308,7 @@ export function GeneralEditorSettingsSection({
'Show or hide the file tree when opening combined diff views.'
)}
keywords={['diff', 'tree', 'file tree', 'combined diff', 'sidebar']}
className="flex items-center justify-between gap-4 py-2"
className="flex items-center justify-between gap-4 py-3"
>
<div className="min-w-0 flex-1 space-y-0.5">
<Label>
@@ -411,6 +407,6 @@ export function GeneralEditorSettingsSection({
}
/>
</SearchableSetting>
</section>
</SettingsGroup>
)
}
@@ -33,7 +33,7 @@ export function MarkdownDefaultViewSetting({
title={title}
description={description}
keywords={['markdown', 'default', 'view', 'source', 'rich', 'editor', 'raw']}
className="flex items-center justify-between gap-4 py-2"
className="flex items-center justify-between gap-4 py-3"
>
<div className="min-w-0 flex-1 space-y-0.5">
<Label>{title}</Label>
@@ -55,6 +55,7 @@ export function SettingsRow({
}: SettingsRowProps): React.JSX.Element {
return (
<div
data-settings-row=""
className={cn(
'flex gap-4',
description ? 'py-3' : 'py-2',
@@ -19,10 +19,11 @@ import { RecentTabOrderControl } from '../appearance/RecentTabOrderControl'
import { SettingsSectionStack } from '../section-layout/SettingsSectionStack'
import { matchesSettingsSearch, type SettingsSearchEntry } from '../search/settings-search'
import { SearchableSetting } from '../section-layout/SearchableSetting'
import { SettingsSubsectionHeader, SettingsSwitchRow } from '../form-controls/SettingsFormControls'
import { SettingsSwitchRow } from '../form-controls/SettingsFormControls'
import { translate } from '@/i18n/i18n'
import { DefaultWindowsProjectRuntimeSetting } from './project-runtime/DefaultWindowsProjectRuntimeSetting'
import { defaultScopeHost } from '@/lib/default-creation-host'
import { SettingsGroup } from '../section-layout/SettingsGroup'
type GeneralSearchEntry = ReturnType<typeof getGeneralNavigationSearchEntries>[number]
@@ -109,10 +110,10 @@ export function GeneralPane({
const visibleSections = [
matchesSettingsSearch(searchQuery, generalNavigationSearchEntries) ? (
<section key="navigation" className="space-y-4">
<SettingsSubsectionHeader
title={translate('auto.components.settings.GeneralPane.d58fccfd84', 'Navigation')}
/>
<SettingsGroup
key="navigation"
title={translate('auto.components.settings.GeneralPane.d58fccfd84', 'Navigation')}
>
<RecentTabOrderControl
ctrlTabOrderMode={settings.ctrlTabOrderMode ?? 'mru'}
keywords={tabOrderKeywords}
@@ -201,7 +202,7 @@ export function GeneralPane({
}
/>
</SearchableSetting>
</section>
</SettingsGroup>
) : null,
matchesSettingsSearch(searchQuery, getGeneralWorkspaceSearchEntries()) ? (
<GeneralWorkspaceSettingsSection
@@ -218,17 +219,14 @@ export function GeneralPane({
searchQuery,
projectRuntimeSearchEntries
) ? (
<section key="project-runtime" className="space-y-4">
<SettingsSubsectionHeader
title={translate(
'auto.components.settings.GeneralPane.projectRuntime',
'Project Runtime'
)}
description={translate(
'auto.components.settings.GeneralPane.projectRuntimeDescription',
'Default runtime for local Windows projects that do not override it.'
)}
/>
<SettingsGroup
key="project-runtime"
title={translate('auto.components.settings.GeneralPane.projectRuntime', 'Project Runtime')}
description={translate(
'auto.components.settings.GeneralPane.projectRuntimeDescription',
'Default runtime for local Windows projects that do not override it.'
)}
>
<DefaultWindowsProjectRuntimeSetting
settings={settings}
updateSettings={updateSettings}
@@ -237,7 +235,7 @@ export function GeneralPane({
wslDistros={wslDistros}
wslCapabilitiesLoading={Boolean(wslCapabilitiesLoading)}
/>
</section>
</SettingsGroup>
) : null,
matchesSettingsSearch(searchQuery, getGeneralEditorSearchEntries()) ? (
<GeneralEditorSettingsSection
@@ -266,7 +264,7 @@ export function GeneralPane({
return (
<div className="space-y-6">
<SettingsSectionStack sections={visibleSections} spacing="section" />
<SettingsSectionStack sections={visibleSections} />
{matchesSettingsSearch(searchQuery, getGeneralSupportSearchEntries()) ? (
<GeneralSupportSection
hasPrecedingSections={visibleSections.some((section) => section !== null)}
@@ -4,9 +4,8 @@ import { ExternalLink, Loader2, Star } from 'lucide-react'
import { useMountedRef } from '@/hooks/useMountedRef'
import { Button } from '../../ui/button'
import { Label } from '../../ui/label'
import { Separator } from '../../ui/separator'
import { SettingsGroup } from '../section-layout/SettingsGroup'
import { SearchableSetting } from '../section-layout/SearchableSetting'
import { SettingsSubsectionHeader } from '../form-controls/SettingsFormControls'
import { translate } from '@/i18n/i18n'
// Do not deep-link to /stargazers: GitHub 404s that page for users without repo write access.
@@ -96,7 +95,7 @@ function SupportSection({
hasPrecedingSections,
onStarClick
}: SupportSectionProps): React.JSX.Element {
// Collapse the leading separator too when gh is unavailable or the repo was already starred.
// Collapse the leading gap too when gh is unavailable or the repo was already starred.
const collapsed = state === 'hidden'
return (
@@ -107,20 +106,18 @@ function SupportSection({
aria-hidden={collapsed}
>
<div className="min-h-0 overflow-hidden">
<div className="space-y-8">
{hasPrecedingSections ? <Separator /> : null}
<div className="space-y-4">
<SettingsSubsectionHeader
title={translate(
'auto.components.settings.GeneralSupportSection.55a87e5fd1',
'Support Orca'
)}
/>
<div className={hasPrecedingSections ? 'pt-2' : undefined}>
<SettingsGroup
title={translate(
'auto.components.settings.GeneralSupportSection.55a87e5fd1',
'Support Orca'
)}
>
{state === 'loading' ? <SupportRowSkeleton /> : null}
{state !== 'loading' && state !== 'hidden' ? (
<SupportRow state={state} onStarClick={onStarClick} />
) : null}
</div>
</SettingsGroup>
</div>
</div>
</section>
@@ -2,11 +2,12 @@ import type React from 'react'
import type { GlobalSettings } from '../../../../../shared/global-settings-types'
import { OpenInMenuSetting } from './OpenInMenuSetting'
import { SearchableSetting } from '../section-layout/SearchableSetting'
import { SettingsSubsectionHeader, SettingsSwitchRow } from '../form-controls/SettingsFormControls'
import { SettingsSwitchRow } from '../form-controls/SettingsFormControls'
import { WorkspaceDirectorySetting } from './WorkspaceDirectorySetting'
import { translate } from '@/i18n/i18n'
import { GlobalWorktreeVisibilitySourcesSetting } from './GlobalWorktreeVisibilitySourcesSetting'
import { GLOBAL_WORKTREE_VISIBILITY_SETTINGS_TARGET_ID } from '@/lib/settings-navigation-types'
import { SettingsGroup } from '../section-layout/SettingsGroup'
type GeneralWorkspaceSettingsSectionProps = {
settings: GlobalSettings
@@ -24,18 +25,17 @@ export function GeneralWorkspaceSettingsSection({
sourceDefaultsSupported = true
}: GeneralWorkspaceSettingsSectionProps): React.JSX.Element {
return (
<section key="workspace" className="space-y-4">
<SettingsSubsectionHeader
title={translate(
'auto.components.settings.GeneralWorkspaceSettingsSection.7511097c5d',
'Workspace'
)}
description={translate(
'auto.components.settings.GeneralWorkspaceSettingsSection.e2955d9ccb',
'Configure where new workspaces are created.'
)}
/>
<SettingsGroup
key="workspace"
title={translate(
'auto.components.settings.GeneralWorkspaceSettingsSection.7511097c5d',
'Workspace'
)}
description={translate(
'auto.components.settings.GeneralWorkspaceSettingsSection.e2955d9ccb',
'Configure where new workspaces are created.'
)}
>
<WorkspaceDirectorySetting settings={settings} updateSettings={updateSettings} />
<div
@@ -52,6 +52,7 @@ export function GeneralWorkspaceSettingsSection({
'auto.components.settings.GeneralWorkspaceSettingsSection.externalWorktreesDescription',
'Choose which worktrees created outside Orca appear by default on this host.'
)}
className="py-3"
keywords={[
'external',
'non-Orca',
@@ -225,7 +226,7 @@ export function GeneralWorkspaceSettingsSection({
'finder',
'file explorer'
]}
className="space-y-3"
className="space-y-3 py-3"
>
<OpenInMenuSetting
applications={settings.openInApplications}
@@ -233,6 +234,6 @@ export function GeneralWorkspaceSettingsSection({
/>
</SearchableSetting>
</div>
</section>
</SettingsGroup>
)
}
@@ -149,7 +149,7 @@ export function WorkspaceDirectorySetting({
'Root directory where workspace folders are created.'
)}
keywords={['workspace', 'folder', 'path', 'worktree', 'host', 'override']}
className="space-y-2"
className="space-y-2 py-3"
>
<div className="flex items-center justify-between gap-2">
<Label htmlFor={inputId}>
@@ -9,52 +9,71 @@ import {
} from '../../integrations/source-control/cli-source-control-integration-cards'
import { JiraIntegrationCard } from '../../integrations/task-trackers/jira-integration-card'
import { LinearIntegrationCard } from '../../integrations/task-trackers/linear-integration-card'
import { IntegrationCardPresentationProvider } from '../../integrations/presentation/integration-card-presentation'
import { useIntegrationProviderStatusRefresh } from '../../../hooks/use-integration-provider-status-refresh'
import { SettingsGroup } from '../section-layout/SettingsGroup'
import { translate } from '@/i18n/i18n'
// Flat presentation turns each provider into one row: -mx-4 cancels its inset, and inner rules are dropped so only rows divide.
function ProviderRow({ children }: { children: React.ReactNode }): React.JSX.Element {
return (
<div data-settings-row="" className="[&_.border-t]:border-t-0 [&>*]:-mx-4">
{children}
</div>
)
}
export function IntegrationsPane(): React.JSX.Element {
useIntegrationProviderStatusRefresh()
return (
<div className="space-y-5">
<section className="space-y-3">
<div className="space-y-1">
<h3 className="text-sm font-semibold text-foreground">
{translate('auto.components.settings.IntegrationsPane.298c65ecac', 'Review providers')}
</h3>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.IntegrationsPane.1683acbac4',
'Connect the source hosts Orca can use for pull requests, merge requests, checks, and review status.'
)}
</p>
</div>
<div className="space-y-3">
<GitHubIntegrationCard />
<GitLabIntegrationCard />
<BitbucketIntegrationCard />
<AzureDevOpsIntegrationCard />
<GiteaIntegrationCard />
</div>
</section>
<IntegrationCardPresentationProvider value="setup-guide">
<div className="space-y-8">
<SettingsGroup
title={translate(
'auto.components.settings.IntegrationsPane.298c65ecac',
'Review providers'
)}
description={translate(
'auto.components.settings.IntegrationsPane.1683acbac4',
'Connect the source hosts Orca can use for pull requests, merge requests, checks, and review status.'
)}
>
<ProviderRow>
<GitHubIntegrationCard />
</ProviderRow>
<ProviderRow>
<GitLabIntegrationCard />
</ProviderRow>
<ProviderRow>
<BitbucketIntegrationCard />
</ProviderRow>
<ProviderRow>
<AzureDevOpsIntegrationCard />
</ProviderRow>
<ProviderRow>
<GiteaIntegrationCard />
</ProviderRow>
</SettingsGroup>
<section className="space-y-3">
<div className="space-y-1">
<h3 className="text-sm font-semibold text-foreground">
{translate('auto.components.settings.IntegrationsPane.70e885705b', 'Task providers')}
</h3>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.IntegrationsPane.3ba07f933b',
'Connect issue trackers Orca can use to browse tasks and start workspaces with linked context.'
)}
</p>
</div>
<div className="space-y-3">
<LinearIntegrationCard />
<JiraIntegrationCard />
</div>
</section>
</div>
<SettingsGroup
title={translate(
'auto.components.settings.IntegrationsPane.70e885705b',
'Task providers'
)}
description={translate(
'auto.components.settings.IntegrationsPane.3ba07f933b',
'Connect issue trackers Orca can use to browse tasks and start workspaces with linked context.'
)}
>
<ProviderRow>
<LinearIntegrationCard />
</ProviderRow>
<ProviderRow>
<JiraIntegrationCard />
</ProviderRow>
</SettingsGroup>
</div>
</IntegrationCardPresentationProvider>
)
}
@@ -13,7 +13,7 @@ export function MobileAutoRestoreFitSection({
onAutoRestoreFitChange
}: MobileAutoRestoreFitSectionProps): React.JSX.Element {
return (
<div className="rounded-lg border border-border/60 p-4">
<div>
<div className="mb-3 flex items-center gap-2">
<Smartphone className="size-4 text-muted-foreground" />
<span className="text-sm font-medium">
@@ -341,84 +341,96 @@ export function MobilePane(): React.JSX.Element {
})
return (
<div className="space-y-6">
<MachineNameField id="mobile-machine-name" />
<>
{/* empty:hidden drops the row (and its rule) when the field renders nothing on web clients. */}
<div data-settings-row="" className="py-4 empty:hidden">
<MachineNameField id="mobile-machine-name" />
</div>
<MobilePairingSetupSection
connectionMode={connectionMode}
canGenerate={canMintMobilePairingOffer({ connectionMode, signedIn })}
addressDisclosureForcedOpen={shouldOpenMobilePairingAddress(settingsSearchQuery)}
connectionPathControl={
<MobilePairingConnectionOptions
value={connectionMode}
onChange={changeConnectionMode}
relayMintFailed={relayMintFailure != null && connectionMode === 'automatic'}
relayMintRetrying={
relayMintFailure != null && connectionMode === 'automatic' && loading
}
/>
}
networkInterfaces={networkInterfaces}
customAddresses={customAddresses}
selectedAddress={selectedAddress}
selectedAddressIsCustom={selectedAddressIsCustom}
onSelectedAddressChange={handleSelectedAddressChange}
onCustomAddressSelect={handleCustomAddressSelect}
onCustomAddressRemove={handleCustomAddressRemove}
refreshingNetworkInterfaces={refreshingNetworkInterfaces}
onRefreshNetworkInterfaces={() => void loadNetworkInterfaces({ notifyOnError: true })}
loading={loading}
hasQrCode={qrDataUrl != null}
showGenerateAction={relayMintFailure == null}
onGenerateQr={() => void generateQR({ rotate: qrDataUrl != null })}
/>
{relayMintFailure != null && connectionMode === 'automatic' ? (
<MobileRelayMintFailureNotice
failure={relayMintFailure}
onUseLan={() => changeConnectionMode('local-only')}
onRetry={() => void generateQR({ rotate: true })}
onCopyDiagnostics={() => void copyRelayDiagnostics()}
busy={loading}
<div data-settings-row="" className="space-y-6 py-4">
<MobilePairingSetupSection
connectionMode={connectionMode}
canGenerate={canMintMobilePairingOffer({ connectionMode, signedIn })}
addressDisclosureForcedOpen={shouldOpenMobilePairingAddress(settingsSearchQuery)}
connectionPathControl={
<MobilePairingConnectionOptions
value={connectionMode}
onChange={changeConnectionMode}
relayMintFailed={relayMintFailure != null && connectionMode === 'automatic'}
relayMintRetrying={
relayMintFailure != null && connectionMode === 'automatic' && loading
}
/>
}
networkInterfaces={networkInterfaces}
customAddresses={customAddresses}
selectedAddress={selectedAddress}
selectedAddressIsCustom={selectedAddressIsCustom}
onSelectedAddressChange={handleSelectedAddressChange}
onCustomAddressSelect={handleCustomAddressSelect}
onCustomAddressRemove={handleCustomAddressRemove}
refreshingNetworkInterfaces={refreshingNetworkInterfaces}
onRefreshNetworkInterfaces={() => void loadNetworkInterfaces({ notifyOnError: true })}
loading={loading}
hasQrCode={qrDataUrl != null}
showGenerateAction={relayMintFailure == null}
onGenerateQr={() => void generateQR({ rotate: qrDataUrl != null })}
/>
) : null}
<span className="sr-only" role="status" aria-live="polite">
{pairingUrl != null && !loading
? translate('auto.components.settings.MobilePane.pairingCodeReady', 'Pairing code ready')
: ''}
</span>
{relayMintFailure != null && connectionMode === 'automatic' ? (
<MobileRelayMintFailureNotice
failure={relayMintFailure}
onUseLan={() => changeConnectionMode('local-only')}
onRetry={() => void generateQR({ rotate: true })}
onCopyDiagnostics={() => void copyRelayDiagnostics()}
busy={loading}
/>
) : null}
<MobilePairingQrSection
qrDataUrl={qrDataUrl}
qrSize={qrSize}
qrError={qrError}
pairingUrl={pairingUrl}
endpoint={endpoint}
refreshesAutomatically={qrExpiresAt != null}
qrEnlarged={qrEnlarged}
codeCopied={codeCopied}
onQrEnlargedChange={setQrEnlarged}
onCodeCopiedChange={setCodeCopied}
onClearCodeCopiedTimer={clearCodeCopiedResetTimer}
/>
<span className="sr-only" role="status" aria-live="polite">
{pairingUrl != null && !loading
? translate(
'auto.components.settings.MobilePane.pairingCodeReady',
'Pairing code ready'
)
: ''}
</span>
<WindowsFirewallNotice
pairingReady={pairingUrl != null}
address={selectedAddress}
usingRelay={connectionMode === 'automatic'}
/>
<MobilePairingQrSection
qrDataUrl={qrDataUrl}
qrSize={qrSize}
qrError={qrError}
pairingUrl={pairingUrl}
endpoint={endpoint}
refreshesAutomatically={qrExpiresAt != null}
qrEnlarged={qrEnlarged}
codeCopied={codeCopied}
onQrEnlargedChange={setQrEnlarged}
onCodeCopiedChange={setCodeCopied}
onClearCodeCopiedTimer={clearCodeCopiedResetTimer}
/>
<MobilePairedDevicesSection
devices={devices}
hasQrCode={qrDataUrl != null}
onRevokeDevice={(deviceId) => void revokeDevice(deviceId)}
/>
<WindowsFirewallNotice
pairingReady={pairingUrl != null}
address={selectedAddress}
usingRelay={connectionMode === 'automatic'}
/>
</div>
<MobileAutoRestoreFitSection
autoRestoreFitMs={autoRestoreFitMs}
onAutoRestoreFitChange={(ms) => void updateSettings({ mobileAutoRestoreFitMs: ms })}
/>
</div>
<div data-settings-row="" className="py-4">
<MobilePairedDevicesSection
devices={devices}
hasQrCode={qrDataUrl != null}
onRevokeDevice={(deviceId) => void revokeDevice(deviceId)}
/>
</div>
<div data-settings-row="" className="py-4">
<MobileAutoRestoreFitSection
autoRestoreFitMs={autoRestoreFitMs}
onAutoRestoreFitChange={(ms) => void updateSettings({ mobileAutoRestoreFitMs: ms })}
/>
</div>
</>
)
}
@@ -1,4 +1,5 @@
import { SearchableSetting } from '../section-layout/SearchableSetting'
import { SettingsGroup } from '../section-layout/SettingsGroup'
import { SettingsSwitchRow } from '../form-controls/SettingsFormControls'
import { MobilePane } from './MobilePane'
import {
@@ -18,79 +19,81 @@ export function MobileSettingsPane(): React.JSX.Element {
const updateSettings = useAppStore((s) => s.updateSettings)
return (
<div className="space-y-4">
<SearchableSetting
title={translate('auto.components.settings.MobileSettingsPane.e7a3ae8c4e', 'Mobile')}
description={translate(
'auto.components.settings.MobileSettingsPane.174f4a3c6d',
'Control terminals and agents from your phone.'
)}
keywords={getMobileOverviewSearchEntry().keywords}
className="space-y-3 py-2"
>
<div className="space-y-2 text-xs text-muted-foreground">
<p>
{translate(
'auto.components.settings.MobileSettingsPane.installIntro',
'Install Orca Mobile from the'
)}{' '}
<button
type="button"
onClick={(event) => openAppHttpLink(ORCA_IOS_APP_STORE_URL, { event })}
className="cursor-pointer underline underline-offset-2 hover:text-foreground"
>
{translate('auto.components.settings.MobileSettingsPane.b5a2ed83ff', 'App Store')}
</button>
{' · '}
<button
type="button"
// Link the pinned release APK until Android distribution moves to Google Play.
onClick={(event) => openAppHttpLink(ORCA_ANDROID_APK_URL, { event })}
className="cursor-pointer underline underline-offset-2 hover:text-foreground"
>
<div className="space-y-8">
<SettingsGroup>
<SearchableSetting
title={translate('auto.components.settings.MobileSettingsPane.e7a3ae8c4e', 'Mobile')}
description={translate(
'auto.components.settings.MobileSettingsPane.174f4a3c6d',
'Control terminals and agents from your phone.'
)}
keywords={getMobileOverviewSearchEntry().keywords}
className="py-3"
>
<div className="space-y-2 text-xs text-muted-foreground">
<p>
{translate(
'auto.components.settings.MobileSettingsPane.androidApkLabel',
'Android APK'
'auto.components.settings.MobileSettingsPane.installIntro',
'Install Orca Mobile from the'
)}{' '}
<button
type="button"
onClick={(event) => openAppHttpLink(ORCA_IOS_APP_STORE_URL, { event })}
className="cursor-pointer underline underline-offset-2 hover:text-foreground"
>
{translate('auto.components.settings.MobileSettingsPane.b5a2ed83ff', 'App Store')}
</button>
{' · '}
<button
type="button"
// Link the pinned release APK until Android distribution moves to Google Play.
onClick={(event) => openAppHttpLink(ORCA_ANDROID_APK_URL, { event })}
className="cursor-pointer underline underline-offset-2 hover:text-foreground"
>
{translate(
'auto.components.settings.MobileSettingsPane.androidApkLabel',
'Android APK'
)}
</button>
{translate(
'auto.components.settings.MobileSettingsPane.installOutro',
', then pair below.'
)}
</button>
{translate(
'auto.components.settings.MobileSettingsPane.installOutro',
', then pair below.'
)}
</p>
<MobileRelayBetaNotice />
</div>
</SearchableSetting>
</p>
<MobileRelayBetaNotice />
</div>
</SearchableSetting>
<SearchableSetting
title={translate(
'auto.components.settings.MobileSettingsPane.1de96ec8a6',
'Show Orca Mobile Button'
)}
description={translate(
'auto.components.settings.MobileSettingsPane.682293cadf',
'Show the Orca Mobile button at the top of the left sidebar.'
)}
keywords={getMobileSidebarShortcutSearchEntry().keywords}
>
{/* Why: the in-page removal toast points users to Settings > Mobile. */}
<SettingsSwitchRow
label={translate(
<SearchableSetting
title={translate(
'auto.components.settings.MobileSettingsPane.1de96ec8a6',
'Show Orca Mobile Button'
)}
description={translate(
'auto.components.settings.MobileSettingsPane.d4f2b65f30',
'Show the Orca Mobile shortcut in the sidebar.'
'auto.components.settings.MobileSettingsPane.682293cadf',
'Show the Orca Mobile button at the top of the left sidebar.'
)}
checked={showMobileButton}
onChange={() => updateSettings({ showMobileButton: !showMobileButton })}
/>
</SearchableSetting>
keywords={getMobileSidebarShortcutSearchEntry().keywords}
>
{/* Why: the in-page removal toast points users to Settings > Mobile. */}
<SettingsSwitchRow
label={translate(
'auto.components.settings.MobileSettingsPane.1de96ec8a6',
'Show Orca Mobile Button'
)}
description={translate(
'auto.components.settings.MobileSettingsPane.d4f2b65f30',
'Show the Orca Mobile shortcut in the sidebar.'
)}
checked={showMobileButton}
onChange={() => updateSettings({ showMobileButton: !showMobileButton })}
/>
</SearchableSetting>
</SettingsGroup>
<div className="rounded-xl border border-border/60 bg-card/50 p-4">
<SettingsGroup>
<MobilePane />
</div>
</SettingsGroup>
</div>
)
}
@@ -4,6 +4,7 @@ import { useMobileEmulatorAgentSetupState } from '../../../emulator-pane/use-mob
import { AgentSkillSetupPanel } from '../../../skills/setup/AgentSkillSetupPanel'
import { MobileEmulatorExamples } from './MobileEmulatorExamples'
import { translate } from '@/i18n/i18n'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
const EMULATOR_CLI_COMMANDS = [
'orca emulator list --json',
@@ -17,25 +18,17 @@ export function MobileEmulatorAgentControlRow(): React.JSX.Element {
const activeSkillRuntime = setup.skillRuntime
return (
<div className="rounded-2xl border border-border/60 bg-card/30 p-4">
<div className="flex items-center justify-between gap-3">
<div className="space-y-0.5">
<p className="text-sm font-semibold">
{translate(
'auto.components.settings.MobileEmulatorAgentControlRow.2a674aa810',
'Agent Mobile Emulator Control'
)}
</p>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.MobileEmulatorAgentControlRow.ff4b7e65d6',
'Let coding agents control the active mobile emulator with Orca CLI commands.'
)}
</p>
</div>
</div>
<div className="mt-3 divide-y divide-border/40">
<SettingsGroup
title={translate(
'auto.components.settings.MobileEmulatorAgentControlRow.2a674aa810',
'Agent Mobile Emulator Control'
)}
description={translate(
'auto.components.settings.MobileEmulatorAgentControlRow.ff4b7e65d6',
'Let coding agents control the active mobile emulator with Orca CLI commands.'
)}
>
<div className="divide-y divide-border">
<div className="py-3">
<AgentSkillSetupPanel
variant="inline"
@@ -91,6 +84,6 @@ export function MobileEmulatorAgentControlRow(): React.JSX.Element {
<MobileEmulatorExamples variant="inline" />
</div>
</div>
</SettingsGroup>
)
}
@@ -10,6 +10,7 @@ import { EmulatorOpeningSettings } from './EmulatorOpeningSettings'
import { MobileEmulatorAgentControlRow } from './MobileEmulatorAgentControlRow'
import { MobileEmulatorAvailabilityDetails } from './MobileEmulatorAvailabilityDetails'
import { SearchableSetting } from '../../section-layout/SearchableSetting'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
import { SettingsSwitchRow } from '../../form-controls/SettingsFormControls'
import { getMobileEmulatorSearchEntries } from './mobile-emulator-search'
import { translate } from '@/i18n/i18n'
@@ -135,90 +136,92 @@ export function MobileEmulatorSettingsPane({
const devices = availability?.devices ?? []
return (
<div className="space-y-4">
<SearchableSetting
title={translate(
'auto.components.settings.MobileEmulatorSettingsPane.6593c9ddd3',
'Mobile Emulator'
)}
description={translate(
'auto.components.settings.MobileEmulatorSettingsPane.bc39d0f115',
'Configure mobile emulator support for Orca and coding agents.'
)}
keywords={getMobileEmulatorSearchEntries().flatMap((entry) => entry.keywords ?? [])}
className="divide-y divide-border/40"
>
<SettingsSwitchRow
label={translate(
'auto.components.settings.MobileEmulatorSettingsPane.700ddbf9b1',
'Enable Mobile Emulator'
<div className="space-y-8">
<SettingsGroup>
<SearchableSetting
title={translate(
'auto.components.settings.MobileEmulatorSettingsPane.6593c9ddd3',
'Mobile Emulator'
)}
description={translate(
'auto.components.settings.MobileEmulatorSettingsPane.f9af91ea26',
'Shows the New Mobile Emulator action and allows agents to attach to the active emulator.'
'auto.components.settings.MobileEmulatorSettingsPane.bc39d0f115',
'Configure mobile emulator support for Orca and coding agents.'
)}
checked={enabled}
onChange={() => updateSettings({ mobileEmulatorEnabled: !enabled })}
/>
keywords={getMobileEmulatorSearchEntries().flatMap((entry) => entry.keywords ?? [])}
className="divide-y divide-border"
>
<SettingsSwitchRow
label={translate(
'auto.components.settings.MobileEmulatorSettingsPane.700ddbf9b1',
'Enable Mobile Emulator'
)}
description={translate(
'auto.components.settings.MobileEmulatorSettingsPane.f9af91ea26',
'Shows the New Mobile Emulator action and allows agents to attach to the active emulator.'
)}
checked={enabled}
onChange={() => updateSettings({ mobileEmulatorEnabled: !enabled })}
/>
<div className="py-2">
<div className="flex items-start gap-4">
<div className="min-w-0 flex-1 space-y-0.5">
<Label>
{translate(
'auto.components.settings.MobileEmulatorSettingsPane.ae1612c58c',
'Availability'
)}
</Label>
<p className="text-xs text-muted-foreground">{availabilityDetail(availability)}</p>
</div>
<div className="flex shrink-0 items-center gap-2">
<Badge
variant="outline"
className={cn('text-[11px]', statusBadgeClassName(availability, enabled))}
>
{refreshing ? <Loader2 className="size-3 animate-spin" /> : null}
{statusText(availability, enabled)}
</Badge>
<Button
type="button"
variant="outline"
size="icon-xs"
aria-label={translate(
'auto.components.settings.MobileEmulatorSettingsPane.8aec2f99a0',
'Refresh emulator availability'
)}
onClick={() => void refreshAvailability()}
disabled={refreshing}
>
{refreshing ? (
<Loader2 className="size-3.5 animate-spin" />
) : (
<RefreshCw className="size-3.5" />
)}
</Button>
<div className="py-3">
<div className="flex items-start gap-4">
<div className="min-w-0 flex-1 space-y-0.5">
<Label>
{translate(
'auto.components.settings.MobileEmulatorSettingsPane.ae1612c58c',
'Availability'
)}
</Label>
<p className="text-xs text-muted-foreground">{availabilityDetail(availability)}</p>
</div>
<div className="flex shrink-0 items-center gap-2">
<Badge
variant="outline"
className={cn('text-[11px]', statusBadgeClassName(availability, enabled))}
>
{refreshing ? <Loader2 className="size-3 animate-spin" /> : null}
{statusText(availability, enabled)}
</Badge>
<Button
type="button"
variant="outline"
size="icon-xs"
aria-label={translate(
'auto.components.settings.MobileEmulatorSettingsPane.8aec2f99a0',
'Refresh emulator availability'
)}
onClick={() => void refreshAvailability()}
disabled={refreshing}
>
{refreshing ? (
<Loader2 className="size-3.5 animate-spin" />
) : (
<RefreshCw className="size-3.5" />
)}
</Button>
</div>
</div>
{enabled ? (
<MobileEmulatorAvailabilityDetails
availability={availability}
configuredPath={settings.androidSdkPath ?? null}
onSetAndroidSdkPath={async (path) => {
await updateSettings({ androidSdkPath: path })
await refreshAvailability()
}}
/>
) : null}
</div>
{enabled ? (
<MobileEmulatorAvailabilityDetails
availability={availability}
configuredPath={settings.androidSdkPath ?? null}
onSetAndroidSdkPath={async (path) => {
await updateSettings({ androidSdkPath: path })
await refreshAvailability()
}}
/>
) : null}
</div>
<EmulatorOpeningSettings
settings={settings}
devices={devices}
enabled={enabled}
updateSettings={updateSettings}
/>
</SearchableSetting>
<EmulatorOpeningSettings
settings={settings}
devices={devices}
enabled={enabled}
updateSettings={updateSettings}
/>
</SearchableSetting>
</SettingsGroup>
{enabled ? (
<SearchableSetting
@@ -29,6 +29,7 @@ import {
getNestedWorkerDepthTitle
} from './nested-worker-depth-copy'
import { OrchestrationWorkerAutoReleaseSetting } from './OrchestrationWorkerAutoReleaseSetting'
import { SettingsGroup } from '../section-layout/SettingsGroup'
const EXAMPLE_ICONS = {
handoff: ArrowRightLeft,
@@ -107,66 +108,70 @@ export function OrchestrationPane({
)}
keywords={searchEntries[0].keywords}
forceVisible
className="space-y-5 py-2"
className="space-y-8"
>
<AgentSkillSetupPanel
title={translate(
'auto.components.settings.OrchestrationPane.07641b9768',
'Orchestration skill'
)}
description={translate(
'auto.components.settings.OrchestrationPane.9bedd2a6e5',
'Enables agents to hand off context and coordinate work through Orca.'
)}
skillNames={[ORCHESTRATION_SKILL_NAME]}
installed={orchestrationSkillDetected}
loading={orchestrationSkillLoading}
error={activeSkillRuntime.installDisabledReason ?? orchestrationSkillError}
installDisabled={Boolean(activeSkillRuntime.installDisabledReason)}
icon={<Workflow className="size-5" />}
onBeforeInstall={() => {
useAppStore.getState().recordFeatureInteraction('agent-orchestration-setup')
}}
// COMPAT(wsl-bridge): removed when WSL runs as an orcad host.
actionHint={
activeSkillRuntime.agentRuntime?.runtime === 'wsl' && !orchestrationSkillDetected ? (
<button
type="button"
className="text-xs text-muted-foreground underline-offset-2 hover:underline"
onClick={() => setSkillPromptOpen(true)}
>
{translate(
'auto.components.settings.OrchestrationPane.7bc082f4de',
'Copy install command'
)}
</button>
) : undefined
}
footer={renderCoverage}
onRecheck={refreshOrchestrationSkill}
freshnessSkillName={
activeSkillRuntime.canUseSkillFreshness ? ORCHESTRATION_SKILL_NAME : undefined
}
/>
{showLocalRuntimeSettings ? (
<NumberField
label={getNestedWorkerDepthTitle()}
description={getNestedWorkerDepthDescription()}
value={resolveNestedWorkerMaxDepth(settings)}
min={1}
integer
onChange={(nestedWorkerMaxDepth) => {
void updateSettings({ nestedWorkerMaxDepth })
}}
/>
) : null}
{showLocalRuntimeSettings ? (
<OrchestrationWorkerAutoReleaseSetting
settings={settings}
updateSettings={updateSettings}
/>
) : null}
<SettingsGroup>
<div className="py-4">
<AgentSkillSetupPanel
variant="inline"
title={translate(
'auto.components.settings.OrchestrationPane.07641b9768',
'Orchestration skill'
)}
description={translate(
'auto.components.settings.OrchestrationPane.9bedd2a6e5',
'Enables agents to hand off context and coordinate work through Orca.'
)}
skillNames={[ORCHESTRATION_SKILL_NAME]}
installed={orchestrationSkillDetected}
loading={orchestrationSkillLoading}
error={activeSkillRuntime.installDisabledReason ?? orchestrationSkillError}
installDisabled={Boolean(activeSkillRuntime.installDisabledReason)}
icon={<Workflow className="size-5" />}
onBeforeInstall={() => {
useAppStore.getState().recordFeatureInteraction('agent-orchestration-setup')
}}
// COMPAT(wsl-bridge): removed when WSL runs as an orcad host.
actionHint={
activeSkillRuntime.agentRuntime?.runtime === 'wsl' && !orchestrationSkillDetected ? (
<button
type="button"
className="text-xs text-muted-foreground underline-offset-2 hover:underline"
onClick={() => setSkillPromptOpen(true)}
>
{translate(
'auto.components.settings.OrchestrationPane.7bc082f4de',
'Copy install command'
)}
</button>
) : undefined
}
footer={renderCoverage}
onRecheck={refreshOrchestrationSkill}
freshnessSkillName={
activeSkillRuntime.canUseSkillFreshness ? ORCHESTRATION_SKILL_NAME : undefined
}
/>
</div>
{showLocalRuntimeSettings ? (
<NumberField
label={getNestedWorkerDepthTitle()}
description={getNestedWorkerDepthDescription()}
value={resolveNestedWorkerMaxDepth(settings)}
min={1}
integer
onChange={(nestedWorkerMaxDepth) => {
void updateSettings({ nestedWorkerMaxDepth })
}}
/>
) : null}
{showLocalRuntimeSettings ? (
<OrchestrationWorkerAutoReleaseSetting
settings={settings}
updateSettings={updateSettings}
/>
) : null}
</SettingsGroup>
{/* COMPAT(wsl-bridge): removed when WSL runs as an orcad host. */}
{activeSkillRuntime.agentRuntime?.runtime === 'wsl' ? (
@@ -19,6 +19,7 @@ export function renderAdvancedSettingsSection(
return model.showDesktopOnlySettings ? (
<SettingsSection
id="advanced"
grouped
title={translate('auto.components.settings.Settings.1c87f8d024', 'Advanced')}
description={translate(
'auto.components.settings.Settings.499c1cd7f9',
@@ -38,6 +39,7 @@ export function renderDevSettingsSection(context: SettingsRenderContext): React.
return model.showDesktopOnlySettings && import.meta.env.DEV ? (
<SettingsSection
id="dev"
grouped
title={translate('auto.components.settings.Settings.dev', 'Dev Tools')}
description={translate(
'auto.components.settings.Settings.devDescription',
@@ -61,6 +63,7 @@ export function renderExperimentalSettingsSection(
return (
<SettingsSection
id="experimental"
grouped
title={translate('auto.components.settings.Settings.8b017f2506', 'Experimental')}
description={translate(
'auto.components.settings.Settings.075341c763',
@@ -18,6 +18,7 @@ export function renderAgentsSettingsSection(context: SettingsRenderContext): Rea
return (
<SettingsSection
id="agents"
grouped
title={translate('auto.components.settings.Settings.8afa676615', 'Agents')}
description={translate(
'auto.components.settings.Settings.ec1ba547f7',
@@ -54,6 +55,7 @@ export function renderAccountsSettingsSection(context: SettingsRenderContext): R
return (
<SettingsSection
id="accounts"
grouped
title={translate('auto.components.settings.Settings.ad6c529693', 'AI Provider Accounts')}
description={translate(
'auto.components.settings.Settings.21f09426ea',
@@ -107,6 +109,7 @@ export function renderOrchestrationSettingsSection(
return (
<SettingsSection
id="orchestration"
grouped
title={translate('auto.components.settings.Settings.00c3a7950d', 'Orchestration')}
description={translate(
'auto.components.settings.Settings.475980f53d',
@@ -128,6 +131,7 @@ export function renderLinearSettingsSection(
return model.linearConnected ? (
<SettingsSection
id="linear"
grouped
title={translate('auto.components.settings.Settings.linearTitle', 'Linear')}
description={translate(
'auto.components.settings.Settings.linearDescription',
@@ -148,6 +152,7 @@ export function renderDesktopCapabilitySettingsSections(
<>
<SettingsSection
id="computer-use"
grouped
title={translate('auto.components.settings.Settings.c9841721cb', 'Computer Use')}
description={translate(
'auto.components.settings.Settings.7118953f14',
@@ -11,6 +11,7 @@ export function renderGitSettingsSection(context: SettingsRenderContext): React.
return (
<SettingsSection
id="git"
grouped
title={translate('auto.components.settings.Settings.70100f94c7', 'Git & Source Control')}
description={translate(
'auto.components.settings.Settings.cfa34f4465',
@@ -54,6 +55,7 @@ export function renderTasksSettingsSection(context: SettingsRenderContext): Reac
return (
<SettingsSection
id="tasks"
grouped
title={translate('auto.components.settings.Settings.11faa2f7dd', 'Task Sources')}
description={translate(
'auto.components.settings.Settings.tasksDescription',
@@ -12,6 +12,7 @@ export function renderTerminalSettingsSection(context: SettingsRenderContext): R
return (
<SettingsSection
id="terminal"
grouped
title={translate('auto.components.settings.Settings.3de4bbb841', 'Terminal')}
description={translate(
'auto.components.settings.Settings.b79b5b31e9',
@@ -69,6 +70,7 @@ export function renderBrowserSettingsSection(
return model.showDesktopOnlySettings ? (
<SettingsSection
id="browser"
grouped
title={translate('auto.components.settings.Settings.c46215ea03', 'Browser')}
description={translate(
'auto.components.settings.Settings.ad9788036f',
@@ -94,6 +96,7 @@ export function renderMobileEmulatorSettingsSection(
return model.showDesktopOnlySettings ? (
<SettingsSection
id="mobile-emulator"
grouped
title={translate('auto.components.settings.Settings.f75daf1002', 'Mobile Emulator')}
description={translate(
'auto.components.settings.Settings.01f9d36292',
@@ -13,6 +13,7 @@ export function renderAppearanceSettingsSection(context: SettingsRenderContext):
return (
<SettingsSection
id="appearance"
grouped
title={translate('auto.components.settings.Settings.2b4474780a', 'Appearance')}
description={translate(
'auto.components.settings.Settings.6d1a27e193',
@@ -40,6 +40,7 @@ export function renderProjectSettingsSections(context: SettingsRenderContext): R
<SettingsSection
key={repoSectionId}
id={repoSectionId}
grouped
title={translate(
'auto.components.settings.Settings.3bf149e873',
'Project Settings > {{value0}}',
@@ -81,6 +81,7 @@ export function renderDeveloperPermissionsSettingsSection(
return model.showDesktopOnlySettings && model.isMac ? (
<SettingsSection
id="developer-permissions"
grouped
title={translate('auto.components.settings.Settings.65660d4548', 'macOS Permissions')}
description={translate(
'auto.components.settings.Settings.9b83cc62c2',
@@ -53,6 +53,7 @@ export function renderGeneralSettingsSection(context: SettingsRenderContext): Re
return (
<SettingsSection
id="general"
grouped
title={translate('auto.components.settings.Settings.7807c11c4d', 'General')}
description={translate(
'auto.components.settings.Settings.f9b77539fd',
@@ -84,6 +85,7 @@ export function renderIntegrationsSettingsSection(
return (
<SettingsSection
id="integrations"
grouped
title={translate('auto.components.settings.Settings.c9ca101a3b', 'Integrations')}
description={translate(
'auto.components.settings.Settings.b07041697f',
@@ -104,6 +106,7 @@ export function renderMobileSettingsSection(
return model.showDesktopOnlySettings ? (
<SettingsSection
id="mobile"
grouped
title={translate('auto.components.settings.Settings.c40dadaac8', 'Mobile')}
badge="Beta"
description={translate(
@@ -124,6 +127,7 @@ export function renderAutomationsSettingsSection(
return (
<SettingsSection
id="automations"
grouped
title={translate('auto.components.settings.automations.title', 'Automations')}
description={translate(
'auto.components.settings.automations.description',
@@ -143,6 +147,7 @@ export function renderArtifactsSettingsSection(context: SettingsRenderContext):
return (
<SettingsSection
id="artifacts"
grouped
title={translate('auto.components.settings.artifacts.title', 'Artifacts')}
badge="Beta"
description={translate(
@@ -165,6 +170,7 @@ export function renderShareSkillsSettingsSection(
return (
<SettingsSection
id="share-skills"
grouped
title={translate('auto.components.settings.shareSkills.title', 'Share Skills')}
badge="Beta"
description={translate(
@@ -185,6 +191,7 @@ export function renderSessionHistorySettingsSection(
return (
<SettingsSection
id="session-history"
grouped
title={translate('sessionHistory.settings.title', 'Agent Session Search')}
description={translate(
'sessionHistory.settings.description',
@@ -10,7 +10,7 @@ import { Input } from '../../ui/input'
import { Label } from '../../ui/label'
import { Textarea } from '../../ui/textarea'
import { SearchableSetting } from '../section-layout/SearchableSetting'
import { SettingsSubsectionHeader } from '../form-controls/SettingsFormControls'
import { SettingsGroup } from '../section-layout/SettingsGroup'
export function getRepositoryFormatterSearchEntry() {
return {
@@ -83,10 +83,9 @@ export function RepositoryFormatterSection({
return (
<SearchableSetting {...entry} forceVisible={forceVisible}>
<section className="space-y-4">
<SettingsSubsectionHeader title={entry.title} description={entry.description} />
<SettingsGroup title={entry.title} description={entry.description}>
<form
className="space-y-4"
className="space-y-4 py-4"
onSubmit={(event) => {
event.preventDefault()
void save()
@@ -145,7 +144,7 @@ export function RepositoryFormatterSection({
: translate('settings.repositoryFormatter.save', 'Save formatter')}
</Button>
</form>
</section>
</SettingsGroup>
</SearchableSetting>
)
}
@@ -187,7 +187,7 @@ export function RepositoryGitHubAccountSection({
fallback: 'github account'
}
])}
className="space-y-3"
className="space-y-3 py-3"
forceVisible={forceVisible}
>
<div className="flex items-start justify-between gap-4">
@@ -68,7 +68,7 @@ export function RepositoryJiraProjectSection({
)}
keywords={['jira', 'project', repo.displayName]}
forceVisible={forceVisible}
className="space-y-2"
className="space-y-2 py-3"
>
<p className="text-sm font-semibold">{label}</p>
<JiraProjectPicker
@@ -7,7 +7,6 @@ import type { SourceControlAiSettingsPatch } from '../../../../../shared/source-
import { getRepoKindLabel, isFolderRepo } from '../../../../../shared/repo-kind'
import { getRepoExecutionHostId, type ExecutionHostId } from '../../../../../shared/execution-host'
import { Label } from '../../ui/label'
import { Separator } from '../../ui/separator'
import { useShallow } from 'zustand/react/shallow'
import { RepositoryHooksSection } from './execution/RepositoryHooksSection'
import { McpConfigSection } from './mcp/McpConfigSection'
@@ -15,6 +14,7 @@ import { WorktreeSymlinksSection } from './execution/WorktreeSymlinksSection'
import { SparsePresetSettingsSection } from './execution/SparsePresetSettingsSection'
import { RepositorySourceControlAiSection } from './source-control/RepositorySourceControlAiSection'
import { SearchableSetting } from '../section-layout/SearchableSetting'
import { SettingsGroup } from '../section-layout/SettingsGroup'
import { RepositoryRemoveProjectButton } from './RepositoryRemoveProjectButton'
import type { SettingsProjectRemovalScope } from '../navigation/settings-project-list'
import { matchesSettingsSearch } from '../search/settings-search'
@@ -232,39 +232,38 @@ export function RepositoryPane({
// Lead with identity, then frequently edited setup commands, before secondary repository controls.
const visibleSections = [
forceFullPaneForRepoMatch || matchesSettingsSearch(searchQuery, identityEntries) ? (
<section key="identity" className="relative space-y-8">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1 pr-12">
<h3 className="text-sm font-semibold">
{translate('auto.components.settings.RepositoryPane.499a437335', 'Identity')}
</h3>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.RepositoryPane.b0a0c14a1c',
'Project-specific display details for the sidebar and tabs.'
)}
</p>
<p className="text-xs text-muted-foreground">
<SettingsGroup
key="identity"
title={translate('auto.components.settings.RepositoryPane.499a437335', 'Identity')}
description={
<>
{translate(
'auto.components.settings.RepositoryPane.b0a0c14a1c',
'Project-specific display details for the sidebar and tabs.'
)}
<span className="block">
{translate('auto.components.settings.RepositoryPane.323debba71', 'Type:')}{' '}
<span className="text-foreground">{getRepoKindLabel(repo)}</span>
</p>
</span>
{isFolder ? (
<p className="text-xs text-muted-foreground">
<span className="block">
{translate(
'auto.components.settings.RepositoryPane.ee5a290616',
'Opened as folder. Git features are unavailable for this workspace.'
)}
</p>
</span>
) : null}
</div>
</>
}
action={
<RepositoryRemoveProjectButton
repo={repo}
removalScope={removalScope}
forceVisible={forceFullPaneForRepoMatch}
removeProject={removeProject}
/>
</div>
}
>
<SearchableSetting
title={translate('auto.components.settings.RepositoryPane.c7ef4415de', 'Display Name')}
description={translate(
@@ -272,7 +271,7 @@ export function RepositoryPane({
'Project-specific display details for the sidebar and tabs.'
)}
keywords={[repo.displayName, repo.path, 'project name', 'repository name']}
className="space-y-2"
className="space-y-2 py-3"
forceVisible={forceFullPaneForRepoMatch}
>
<Label htmlFor={`repo-display-name-${repo.id}`} className="text-sm font-semibold">
@@ -303,7 +302,7 @@ export function RepositoryPane({
'emoji',
'favicon'
]}
className="space-y-2"
className="space-y-2 py-3"
id={getRepositoryIconSectionId(repo.id)}
forceVisible={forceFullPaneForRepoMatch}
>
@@ -377,7 +376,7 @@ export function RepositoryPane({
/>
</>
) : null}
</section>
</SettingsGroup>
) : null,
hooksSection,
forceFullPaneForRepoMatch ||
@@ -414,12 +413,7 @@ export function RepositoryPane({
return (
<div ref={setRepositoryPaneRootRef} className="space-y-8">
{visibleSections.map((section, index) => (
<div key={index} className="space-y-8">
{index > 0 ? <Separator /> : null}
{section}
</div>
))}
{visibleSections}
</div>
)
}
@@ -56,7 +56,7 @@ export function RepositoryRemoveProjectButton({
title={translate('auto.components.settings.RepositoryPane.0909e5d650', 'Remove Project')}
description={getRemoveProjectDescription(removalScope)}
keywords={[repo.displayName, 'delete', 'project', 'repository']}
className="absolute top-0 right-0 z-10 w-auto max-w-none"
className="w-auto"
forceVisible={forceVisible}
>
<Tooltip>
@@ -193,7 +193,7 @@ export function RepositoryForkSyncSection({
fallback: 'default branch'
}
])}
className="space-y-3"
className="space-y-3 py-3"
forceVisible={forceVisible}
>
<div className="flex items-start justify-between gap-4">
@@ -13,6 +13,7 @@ import { resolveHookCommandSourcePolicy } from '../../../../../../shared/hook-co
import { useAppStore } from '@/store/index'
import { translate } from '@/i18n/i18n'
import { SearchableSetting } from '../../section-layout/SearchableSetting'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
import { getRepositoryLocalCommandsSectionId } from '../repository-settings-targets'
import {
getLocalHookFields,
@@ -103,158 +104,167 @@ export function RepositoryHooksSection({
const [isAdvancedOpen, setIsAdvancedOpen] = useState(false)
return (
<section ref={flushScriptDraftOnUnmount} className="space-y-6">
<div className="space-y-1">
<h2 className="text-sm font-semibold">
{translate(
'auto.components.settings.RepositoryHooksSection.ff082fe7c6',
'Worktree Hooks'
<div ref={flushScriptDraftOnUnmount}>
<SettingsGroup
title={translate(
'auto.components.settings.RepositoryHooksSection.ff082fe7c6',
'Worktree Hooks'
)}
description={translate(
'auto.components.settings.RepositoryHooksSection.8567127a40',
'Scripts that run when worktrees are created or archived. Local scripts are stored on this machine; `orca.yaml` scripts are shared with your team.'
)}
>
<SearchableSetting
title={translate(
'auto.components.settings.RepositoryHooksSection.52b31baf02',
'Setup Script'
)}
</h2>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.RepositoryHooksSection.8567127a40',
'Scripts that run when worktrees are created or archived. Local scripts are stored on this machine; `orca.yaml` scripts are shared with your team.'
description={translate(
'auto.components.settings.RepositoryHooksSection.30d555acd2',
'Local and shared scripts that run after a new worktree is created.'
)}
</p>
</div>
<SearchableSetting
title={translate(
'auto.components.settings.RepositoryHooksSection.52b31baf02',
'Setup Script'
)}
description={translate(
'auto.components.settings.RepositoryHooksSection.30d555acd2',
'Local and shared scripts that run after a new worktree is created.'
)}
forceVisible={forceVisible}
keywords={[
'setup',
'script',
'command',
'local',
'local settings scripts',
'orca.yaml',
'orca.yaml hooks',
'hook'
]}
>
<RepositoryHookScriptSetting
key={`${repo.id}:setup`}
field={localHookFields[0]}
value={hookSettingsDraft.scripts.setup ?? ''}
hasShared={hasSharedSetupScript}
sharedScript={sharedSetupScript}
onChange={(next) => updateScriptDraft('setup', next)}
onCommit={commitScriptDraft}
sectionId={getRepositoryLocalCommandsSectionId(repo.id)}
/>
</SearchableSetting>
<SearchableSetting
title={translate(
'auto.components.settings.RepositoryHooksSection.fb6bebcf7e',
'When to Run Setup'
)}
description={translate(
'auto.components.settings.RepositoryHooksSection.63e1783173',
'Choose the default behavior when a setup script is available.'
)}
forceVisible={forceVisible}
keywords={['setup run policy', 'ask', 'run by default', 'skip by default']}
>
<RepositorySetupPolicySetting
setupRunPolicy={selectedSetupRunPolicy}
setupAgentStartupPolicy={selectedSetupAgentStartupPolicy}
onRunPolicyChange={(setupRunPolicy) => updateHookSettingsPolicyDraft({ setupRunPolicy })}
onStartupPolicyChange={(setupAgentStartupPolicy) =>
updateHookSettingsPolicyDraft({ setupAgentStartupPolicy })
}
/>
</SearchableSetting>
<SearchableSetting
title={translate(
'auto.components.settings.RepositoryHooksSection.9a100323ff',
'Archive Script'
)}
description={translate(
'auto.components.settings.RepositoryHooksSection.b91a0f297d',
'Local and shared scripts that run before a worktree is archived.'
)}
forceVisible={forceVisible}
keywords={[
'archive',
'script',
'command',
'local',
'local settings scripts',
'orca.yaml',
'orca.yaml hooks',
'hook'
]}
>
<RepositoryHookScriptSetting
key={`${repo.id}:archive`}
field={localHookFields[1]}
value={hookSettingsDraft.scripts.archive ?? ''}
hasShared={hasSharedArchiveScript}
sharedScript={sharedArchiveScript}
onChange={(next) => updateScriptDraft('archive', next)}
onCommit={commitScriptDraft}
/>
</SearchableSetting>
{localCommandSourceNotice ? (
<LocalCommandSourceNotice
notice={localCommandSourceNotice}
onSelectPolicy={(commandSourcePolicy) =>
updateHookSettingsPolicyDraft({ commandSourcePolicy })
}
/>
) : null}
<SearchableSetting
title={translate(
'auto.components.settings.RepositoryHooksSection.13394103bd',
'Custom GitHub Issue Command'
)}
description={translate(
'auto.components.settings.RepositoryHooksSection.2cc27dc12b',
'Optional per-user override for the linked-issue command.'
)}
forceVisible={forceVisible}
keywords={['github issue command', 'issue command', 'workflow', 'agent', 'github']}
>
<RepositoryIssueCommandSetting {...issueCommand} />
</SearchableSetting>
<SearchableSetting
title={translate('auto.components.settings.RepositoryHooksSection.c9bc1bfd8f', 'Advanced')}
description={translate(
'auto.components.settings.RepositoryHooksSection.610d90fdbd',
'Command source and orca.yaml details.'
)}
forceVisible={forceVisible}
keywords={[
'advanced',
'command source',
'orca.yaml',
'shared',
'local',
'both',
'authoritative'
]}
>
<RepositoryHookCommandSourceSetting
searchQuery={settingsSearchQuery}
selectedPolicy={selectedCommandSourcePolicy}
yamlState={yamlState}
yamlHooks={yamlHooks}
copiedTemplate={copiedTemplate}
onSelectPolicy={(commandSourcePolicy) =>
updateHookSettingsPolicyDraft({ commandSourcePolicy })
}
onCopyTemplate={onCopyTemplate}
isAdvancedOpen={isAdvancedOpen}
onAdvancedOpenChange={setIsAdvancedOpen}
/>
</SearchableSetting>
</section>
className="py-4"
forceVisible={forceVisible}
keywords={[
'setup',
'script',
'command',
'local',
'local settings scripts',
'orca.yaml',
'orca.yaml hooks',
'hook'
]}
>
<RepositoryHookScriptSetting
key={`${repo.id}:setup`}
field={localHookFields[0]}
value={hookSettingsDraft.scripts.setup ?? ''}
hasShared={hasSharedSetupScript}
sharedScript={sharedSetupScript}
onChange={(next) => updateScriptDraft('setup', next)}
onCommit={commitScriptDraft}
sectionId={getRepositoryLocalCommandsSectionId(repo.id)}
/>
</SearchableSetting>
<SearchableSetting
title={translate(
'auto.components.settings.RepositoryHooksSection.fb6bebcf7e',
'When to Run Setup'
)}
description={translate(
'auto.components.settings.RepositoryHooksSection.63e1783173',
'Choose the default behavior when a setup script is available.'
)}
className="py-4"
forceVisible={forceVisible}
keywords={['setup run policy', 'ask', 'run by default', 'skip by default']}
>
<RepositorySetupPolicySetting
setupRunPolicy={selectedSetupRunPolicy}
setupAgentStartupPolicy={selectedSetupAgentStartupPolicy}
onRunPolicyChange={(setupRunPolicy) =>
updateHookSettingsPolicyDraft({ setupRunPolicy })
}
onStartupPolicyChange={(setupAgentStartupPolicy) =>
updateHookSettingsPolicyDraft({ setupAgentStartupPolicy })
}
/>
</SearchableSetting>
<SearchableSetting
title={translate(
'auto.components.settings.RepositoryHooksSection.9a100323ff',
'Archive Script'
)}
description={translate(
'auto.components.settings.RepositoryHooksSection.b91a0f297d',
'Local and shared scripts that run before a worktree is archived.'
)}
className="py-4"
forceVisible={forceVisible}
keywords={[
'archive',
'script',
'command',
'local',
'local settings scripts',
'orca.yaml',
'orca.yaml hooks',
'hook'
]}
>
<RepositoryHookScriptSetting
key={`${repo.id}:archive`}
field={localHookFields[1]}
value={hookSettingsDraft.scripts.archive ?? ''}
hasShared={hasSharedArchiveScript}
sharedScript={sharedArchiveScript}
onChange={(next) => updateScriptDraft('archive', next)}
onCommit={commitScriptDraft}
/>
</SearchableSetting>
{localCommandSourceNotice ? (
<div data-settings-row="" className="py-4">
<LocalCommandSourceNotice
notice={localCommandSourceNotice}
onSelectPolicy={(commandSourcePolicy) =>
updateHookSettingsPolicyDraft({ commandSourcePolicy })
}
/>
</div>
) : null}
<SearchableSetting
title={translate(
'auto.components.settings.RepositoryHooksSection.13394103bd',
'Custom GitHub Issue Command'
)}
description={translate(
'auto.components.settings.RepositoryHooksSection.2cc27dc12b',
'Optional per-user override for the linked-issue command.'
)}
className="py-4"
forceVisible={forceVisible}
keywords={['github issue command', 'issue command', 'workflow', 'agent', 'github']}
>
<RepositoryIssueCommandSetting {...issueCommand} />
</SearchableSetting>
<SearchableSetting
title={translate(
'auto.components.settings.RepositoryHooksSection.c9bc1bfd8f',
'Advanced'
)}
description={translate(
'auto.components.settings.RepositoryHooksSection.610d90fdbd',
'Command source and orca.yaml details.'
)}
className="py-4"
forceVisible={forceVisible}
keywords={[
'advanced',
'command source',
'orca.yaml',
'shared',
'local',
'both',
'authoritative'
]}
>
<RepositoryHookCommandSourceSetting
searchQuery={settingsSearchQuery}
selectedPolicy={selectedCommandSourcePolicy}
yamlState={yamlState}
yamlHooks={yamlHooks}
copiedTemplate={copiedTemplate}
onSelectPolicy={(commandSourcePolicy) =>
updateHookSettingsPolicyDraft({ commandSourcePolicy })
}
onCopyTemplate={onCopyTemplate}
isAdvancedOpen={isAdvancedOpen}
onAdvancedOpenChange={setIsAdvancedOpen}
/>
</SearchableSetting>
</SettingsGroup>
</div>
)
}
@@ -170,7 +170,7 @@ export function RepositoryHostSetupsSection({
'Hosts where this project is set up.'
)}
keywords={[repo.displayName, 'host', 'ssh', 'remote', 'vm', 'path']}
className="space-y-3"
className="space-y-3 py-3"
forceVisible={forceVisible}
>
<div className="space-y-1">
@@ -65,7 +65,7 @@ export function RepositoryWorktreeDefaultsSection({
'Override whether worktrees created outside Orca appear for this project.'
)}
keywords={[repo.displayName, 'external', 'non-Orca', 'visibility', 'sidebar']}
className="space-y-2"
className="space-y-2 py-3"
forceVisible={forceVisible}
>
<div className="flex items-center justify-between gap-3">
@@ -132,7 +132,7 @@ export function RepositoryWorktreeDefaultsSection({
'Default base branch or ref when creating worktrees.'
)}
keywords={[repo.displayName, 'base ref', 'branch']}
className="space-y-3"
className="space-y-3 py-3"
forceVisible={forceVisible}
>
<Label className="text-sm font-semibold">
@@ -161,7 +161,7 @@ export function RepositoryWorktreeDefaultsSection({
'relative',
'../worktrees'
]}
className="space-y-2"
className="space-y-2 py-3"
forceVisible={forceVisible}
>
<div className="flex items-center justify-between gap-3">
@@ -10,6 +10,7 @@ import { SparsePresetDraftEditor } from './sparse-preset-draft-editor'
import type { SparsePresetDraft } from '../../../sparse/SparseCheckoutPresetDraftForm'
import { SparsePresetSettingsRow } from './sparse-preset-settings-row'
import { translate } from '@/i18n/i18n'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
type SparsePresetSettingsSectionProps = {
repoId: string
@@ -202,113 +203,111 @@ export function SparsePresetSettingsSection({
) : null
return (
<section ref={sectionRef} className="space-y-4">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<h3 className="text-sm font-semibold">
<section ref={sectionRef}>
<SettingsGroup
title={translate(
'auto.components.settings.SparsePresetSettingsSection.388513be2d',
'Sparse Checkout Presets'
)}
description={translate(
'auto.components.settings.SparsePresetSettingsSection.17f8c4ce10',
'Manage saved directory sets for sparse worktree creation.'
)}
action={
<Button
type="button"
variant="outline"
size="sm"
onClick={startNewPreset}
disabled={!!draft || presets === undefined || deletingPresetId !== null}
>
<Plus className="size-3.5" />
{translate(
'auto.components.settings.SparsePresetSettingsSection.388513be2d',
'Sparse Checkout Presets'
'auto.components.settings.SparsePresetSettingsSection.d7565029a9',
'New Preset'
)}
</h3>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.SparsePresetSettingsSection.17f8c4ce10',
'Manage saved directory sets for sparse worktree creation.'
)}
</p>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={startNewPreset}
disabled={!!draft || presets === undefined || deletingPresetId !== null}
>
<Plus className="size-3.5" />
{translate(
'auto.components.settings.SparsePresetSettingsSection.d7565029a9',
'New Preset'
)}
</Button>
</div>
</Button>
}
>
{visibleError ? (
<div
role="alert"
className="my-3 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive"
>
{visibleError}
</div>
) : null}
{visibleError ? (
<div
role="alert"
className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive"
>
{visibleError}
</div>
) : null}
{draft?.mode === 'new' ? <div className="py-3">{draftEditor}</div> : null}
{draft?.mode === 'new' ? draftEditor : null}
{presets === undefined ? (
<div className="rounded-xl border border-dashed border-border/60 bg-background/60 px-4 py-6 text-sm text-muted-foreground">
{loadError
? translate(
'auto.components.settings.SparsePresetSettingsSection.92c08ccae3',
'Sparse presets could not be loaded.'
)
: translate(
'auto.components.settings.SparsePresetSettingsSection.8deb7024ab',
'Loading sparse presets...'
)}
{loadError ? (
<div className="mt-3">
<Button
type="button"
variant="outline"
size="sm"
disabled={loadStatus === 'loading'}
onClick={() => {
setOperationError(null)
void fetchSparsePresets(repoId).catch((error: unknown) => {
if (mountedRef.current) {
setOperationError(
getSparsePresetOperationErrorMessage(
error,
'Failed to load sparse presets.'
{presets === undefined ? (
<div className="py-4 text-sm text-muted-foreground">
{loadError
? translate(
'auto.components.settings.SparsePresetSettingsSection.92c08ccae3',
'Sparse presets could not be loaded.'
)
: translate(
'auto.components.settings.SparsePresetSettingsSection.8deb7024ab',
'Loading sparse presets...'
)}
{loadError ? (
<div className="mt-3">
<Button
type="button"
variant="outline"
size="sm"
disabled={loadStatus === 'loading'}
onClick={() => {
setOperationError(null)
void fetchSparsePresets(repoId).catch((error: unknown) => {
if (mountedRef.current) {
setOperationError(
getSparsePresetOperationErrorMessage(
error,
'Failed to load sparse presets.'
)
)
)
}
})
}}
>
<RefreshCcw />
{translate('sparsePreset.retryLoad', 'Retry loading presets')}
</Button>
</div>
) : null}
</div>
) : sortedPresets.length === 0 && !draft ? (
<div className="rounded-xl border border-dashed border-border/60 bg-background/60 px-4 py-6 text-sm text-muted-foreground">
{translate(
'auto.components.settings.SparsePresetSettingsSection.88bfbf1a9c',
'No sparse presets saved for this repository.'
)}
</div>
) : (
<div className="space-y-2">
{sortedPresets.map((preset) =>
draft?.presetId === preset.id ? (
<div key={preset.id}>{draftEditor}</div>
) : (
<SparsePresetSettingsRow
key={preset.id}
preset={preset}
confirmingDeleteId={confirmingDeleteId}
deletingPresetId={deletingPresetId}
submitting={submitting || !!draft}
onEdit={startEditPreset}
onDelete={handleDeletePreset}
onClearDeleteConfirm={() => setConfirmingDeleteId(null)}
/>
)
)}
</div>
)}
}
})
}}
>
<RefreshCcw />
{translate('sparsePreset.retryLoad', 'Retry loading presets')}
</Button>
</div>
) : null}
</div>
) : sortedPresets.length === 0 && !draft ? (
<div className="py-4 text-sm text-muted-foreground">
{translate(
'auto.components.settings.SparsePresetSettingsSection.88bfbf1a9c',
'No sparse presets saved for this repository.'
)}
</div>
) : (
<div>
{sortedPresets.map((preset) =>
draft?.presetId === preset.id ? (
<div key={preset.id} data-settings-row="" className="py-3">
{draftEditor}
</div>
) : (
<SparsePresetSettingsRow
key={preset.id}
preset={preset}
confirmingDeleteId={confirmingDeleteId}
deletingPresetId={deletingPresetId}
submitting={submitting || !!draft}
onEdit={startEditPreset}
onDelete={handleDeletePreset}
onClearDeleteConfirm={() => setConfirmingDeleteId(null)}
/>
)
)}
</div>
)}
</SettingsGroup>
</section>
)
}
@@ -16,6 +16,7 @@ import {
getRepoExecutionHostId,
LOCAL_EXECUTION_HOST_ID
} from '../../../../../../shared/execution-host'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
type WorktreeSymlinksSectionProps = {
repo: Repo
@@ -121,167 +122,165 @@ export function WorktreeSymlinksSection({
'env',
'node_modules'
]}
className="space-y-4"
>
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<h3 className="text-sm font-semibold">
{translate(
'auto.components.settings.WorktreeSymlinksSection.4755f120b6',
'Worktree Shared Paths'
)}
</h3>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.WorktreeSymlinksSection.7ff265071d',
'When a new worktree is created, each path listed here is APFS clone-copied on macOS when possible, otherwise symlinked from the primary checkout.'
)}
</p>
</div>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button type="button" variant="outline" size="sm">
<Plus className="size-3.5" />
{translate('auto.components.settings.WorktreeSymlinksSection.241325302c', 'Add Path')}
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="w-72 p-0">
<Command shouldFilter={false}>
<CommandInput
placeholder={translate(
'auto.components.settings.WorktreeSymlinksSection.4cd2a4c077',
'Type a path (e.g. .env or node_modules)…'
<SettingsGroup
title={translate(
'auto.components.settings.WorktreeSymlinksSection.4755f120b6',
'Worktree Shared Paths'
)}
description={translate(
'auto.components.settings.WorktreeSymlinksSection.7ff265071d',
'When a new worktree is created, each path listed here is APFS clone-copied on macOS when possible, otherwise symlinked from the primary checkout.'
)}
action={
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button type="button" variant="outline" size="sm">
<Plus className="size-3.5" />
{translate(
'auto.components.settings.WorktreeSymlinksSection.241325302c',
'Add Path'
)}
value={query}
onValueChange={setQuery}
/>
<CommandList>
<CommandEmpty>
{translate(
'auto.components.settings.WorktreeSymlinksSection.ab40b8a5f1',
'No matches. Keep typing to add a custom path.'
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="w-72 p-0">
<Command shouldFilter={false}>
<CommandInput
placeholder={translate(
'auto.components.settings.WorktreeSymlinksSection.4cd2a4c077',
'Type a path (e.g. .env or node_modules)…'
)}
</CommandEmpty>
{showLiteralItem ? (
<CommandItem
value={`__literal__:${queryTrimmed}`}
onSelect={() => commit(queryTrimmed)}
className="items-center gap-2 px-3 py-2"
>
<Plus className="size-3.5 text-muted-foreground" />
<span className="text-xs">
{translate(
'auto.components.settings.WorktreeSymlinksSection.b2429aeb31',
'Add'
)}{' '}
<code className="rounded bg-muted px-1 py-0.5 text-[11px]">
{queryTrimmed}
</code>
</span>
</CommandItem>
) : null}
{filtered.map((entry) => {
const alreadyAdded = paths.includes(entry.name)
return (
value={query}
onValueChange={setQuery}
/>
<CommandList>
<CommandEmpty>
{translate(
'auto.components.settings.WorktreeSymlinksSection.ab40b8a5f1',
'No matches. Keep typing to add a custom path.'
)}
</CommandEmpty>
{showLiteralItem ? (
<CommandItem
key={entry.name}
value={entry.name}
disabled={alreadyAdded}
onSelect={() => commit(entry.name)}
className={cn('items-center gap-2 px-3 py-2', alreadyAdded && 'opacity-50')}
value={`__literal__:${queryTrimmed}`}
onSelect={() => commit(queryTrimmed)}
className="items-center gap-2 px-3 py-2"
>
{entry.isDirectory ? (
<FolderIcon
folderPath={entry.name}
open={false}
className="size-3.5 text-muted-foreground"
/>
) : (
<FileTypeIcon
filePath={entry.name}
className="size-3.5 text-muted-foreground"
/>
)}
<span className="truncate text-xs">{entry.name}</span>
{alreadyAdded ? (
<span className="ml-auto text-[10px] uppercase tracking-wide text-muted-foreground">
{translate(
'auto.components.settings.WorktreeSymlinksSection.ea06227efa',
'added'
)}
</span>
) : null}
<Plus className="size-3.5 text-muted-foreground" />
<span className="text-xs">
{translate(
'auto.components.settings.WorktreeSymlinksSection.b2429aeb31',
'Add'
)}{' '}
<code className="rounded bg-muted px-1 py-0.5 text-[11px]">
{queryTrimmed}
</code>
</span>
</CommandItem>
)
})}
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
{paths.length === 0 ? (
<div className="rounded-xl border border-dashed border-border/60 bg-background/60 px-4 py-6 text-sm text-muted-foreground">
{translate(
'auto.components.settings.WorktreeSymlinksSection.31ebab5403',
'No shared paths configured for this repository.'
)}
</div>
) : (
<div className="rounded-xl border border-border/50 bg-background/70 px-4 py-3 shadow-sm">
<div className="flex items-start gap-3">
<div className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg border border-border/50 bg-muted/30">
<Link2 className="size-4 text-muted-foreground" />
</div>
<div className="min-w-0 flex-1 space-y-2">
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
<h4 className="text-sm font-medium">
{translate(
'auto.components.settings.WorktreeSymlinksSection.b814c618e2',
'Linked paths'
)}
</h4>
<span className="text-[11px] text-muted-foreground">
{paths.length === 1
? translate(
'auto.components.settings.WorktreeSymlinksSection.9ea912d811',
'1 path'
)
: translate(
'auto.components.settings.WorktreeSymlinksSection.d72ba8dc68',
'{{value0}} paths',
{ value0: paths.length }
)}
</span>
) : null}
{filtered.map((entry) => {
const alreadyAdded = paths.includes(entry.name)
return (
<CommandItem
key={entry.name}
value={entry.name}
disabled={alreadyAdded}
onSelect={() => commit(entry.name)}
className={cn('items-center gap-2 px-3 py-2', alreadyAdded && 'opacity-50')}
>
{entry.isDirectory ? (
<FolderIcon
folderPath={entry.name}
open={false}
className="size-3.5 text-muted-foreground"
/>
) : (
<FileTypeIcon
filePath={entry.name}
className="size-3.5 text-muted-foreground"
/>
)}
<span className="truncate text-xs">{entry.name}</span>
{alreadyAdded ? (
<span className="ml-auto text-[10px] uppercase tracking-wide text-muted-foreground">
{translate(
'auto.components.settings.WorktreeSymlinksSection.ea06227efa',
'added'
)}
</span>
) : null}
</CommandItem>
)
})}
</CommandList>
</Command>
</PopoverContent>
</Popover>
}
>
{paths.length === 0 ? (
<div className="py-4 text-sm text-muted-foreground">
{translate(
'auto.components.settings.WorktreeSymlinksSection.31ebab5403',
'No shared paths configured for this repository.'
)}
</div>
) : (
<div className="py-4">
<div className="flex items-start gap-3">
<div className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg border border-border/50 bg-muted/30">
<Link2 className="size-4 text-muted-foreground" />
</div>
<div className="flex flex-wrap gap-1.5">
{paths.map((path) => (
<span
key={path}
title={path}
className="inline-flex min-w-0 max-w-full items-center gap-1 truncate rounded-md border border-border/50 bg-muted/35 py-1 pl-2 pr-1 font-mono text-[11px] text-foreground/80"
>
<span className="truncate">{path}</span>
<Button
size="icon-xs"
variant="ghost"
onClick={() => handleRemove(path)}
aria-label={translate(
'auto.components.settings.WorktreeSymlinksSection.1c1e35b219',
'Remove {{value0}}',
{ value0: path }
)}
className="size-4 shrink-0 rounded-sm"
>
<X className="size-3" />
</Button>
<div className="min-w-0 flex-1 space-y-2">
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
<h4 className="text-sm font-medium">
{translate(
'auto.components.settings.WorktreeSymlinksSection.b814c618e2',
'Linked paths'
)}
</h4>
<span className="text-[11px] text-muted-foreground">
{paths.length === 1
? translate(
'auto.components.settings.WorktreeSymlinksSection.9ea912d811',
'1 path'
)
: translate(
'auto.components.settings.WorktreeSymlinksSection.d72ba8dc68',
'{{value0}} paths',
{ value0: paths.length }
)}
</span>
))}
</div>
<div className="flex flex-wrap gap-1.5">
{paths.map((path) => (
<span
key={path}
title={path}
className="inline-flex min-w-0 max-w-full items-center gap-1 truncate rounded-md border border-border/50 bg-muted/35 py-1 pl-2 pr-1 font-mono text-[11px] text-foreground/80"
>
<span className="truncate">{path}</span>
<Button
size="icon-xs"
variant="ghost"
onClick={() => handleRemove(path)}
aria-label={translate(
'auto.components.settings.WorktreeSymlinksSection.1c1e35b219',
'Remove {{value0}}',
{ value0: path }
)}
className="size-4 shrink-0 rounded-sm"
>
<X className="size-3" />
</Button>
</span>
))}
</div>
</div>
</div>
</div>
</div>
)}
)}
</SettingsGroup>
</SearchableSetting>
)
}
@@ -30,7 +30,7 @@ export function SparsePresetSettingsRow({
const isDeleting = deletingPresetId === preset.id
return (
<div className="rounded-xl border border-border/50 bg-background/70 px-4 py-3 shadow-sm">
<div data-settings-row="" className="py-3">
<div className="flex items-start gap-3">
<div className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg border border-border/50 bg-muted/30">
<Bookmark className="size-4 text-muted-foreground" />
@@ -50,7 +50,7 @@ function serverDetailLabel(server: LoadedMcpConfigInspection['servers'][number])
export function McpConfigFileRow({ config, onOpen }: McpConfigFileRowProps): React.JSX.Element {
return (
<div className="space-y-2 px-3 py-2.5">
<div data-settings-row="" className="space-y-2 py-3">
<div className="flex items-center gap-2">
{config.status === 'valid' && !config.readError ? (
<CheckCircle2 className="size-3.5 shrink-0 text-muted-foreground" />
@@ -44,6 +44,7 @@ import {
import type { RuntimeFileOperationArgs } from '@/runtime/files/runtime-file-client-types'
import { runtimeTargetForOwnerEnvironment } from '@/runtime/rpc/targets/runtime-client-target'
import { selectWorktreesByRepo } from '@/store/workspaces/catalog/workspace-record-fields'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
type McpConfigSectionProps = {
repo: Repo
@@ -297,28 +298,26 @@ export function McpConfigSection({ repo }: McpConfigSectionProps): React.JSX.Ele
}
return (
<section className="space-y-4">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<h3 className="text-sm font-semibold">
{translate('auto.components.settings.McpConfigSection.55eea3ef47', 'MCP Configs')}
</h3>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.McpConfigSection.96f5609b04',
'Inspect MCP server definitions that agents can use while working in this repo.'
)}
</p>
<SettingsGroup
title={translate('auto.components.settings.McpConfigSection.55eea3ef47', 'MCP Configs')}
description={
<>
{translate(
'auto.components.settings.McpConfigSection.96f5609b04',
'Inspect MCP server definitions that agents can use while working in this repo.'
)}
{connectionId ? (
<p className="text-xs text-muted-foreground">
<span className="block">
{translate(
'auto.components.settings.McpConfigSection.6bac9ddfc6',
'SSH repos are read through the remote filesystem. Starter creation is limited to the workspace root config.'
)}
</p>
</span>
) : null}
</div>
<div className="flex shrink-0 items-center gap-2">
</>
}
action={
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon-sm"
@@ -354,47 +353,47 @@ export function McpConfigSection({ repo }: McpConfigSectionProps): React.JSX.Ele
</Button>
) : null}
</div>
}
>
<div
data-settings-row=""
className="flex items-center justify-between py-3 text-xs text-muted-foreground"
>
<span>{formatMcpConfigSummary(configs)}</span>
{loading ? <LoaderCircle className="size-3.5 animate-spin" /> : null}
</div>
<div className="rounded-md border border-border/50 bg-muted/20">
<div className="flex items-center justify-between border-b border-border/50 px-3 py-2 text-xs text-muted-foreground">
<span>{formatMcpConfigSummary(configs)}</span>
{loading ? <LoaderCircle className="size-3.5 animate-spin" /> : null}
</div>
<div>
{visibleConfigs.length === 0 ? (
<div className="flex items-start gap-2 px-3 py-2.5 text-xs text-muted-foreground">
{inspectionUnavailable ? (
<AlertCircle className="mt-0.5 size-3.5 shrink-0" />
) : (
<FileCode2 className="mt-0.5 size-3.5 shrink-0" />
)}
{inspectionUnavailable ? (
<span>{inspectionUnavailableMessage}</span>
) : (
<span>
{translate(
'auto.components.settings.McpConfigSection.b900cd6282',
'No MCP config found. Add an empty workspace config when you want this repo to define its own MCP servers.'
)}
</span>
)}
</div>
{visibleConfigs.length === 0 ? (
<div
data-settings-row=""
className="flex items-start gap-2 py-3 text-xs text-muted-foreground"
>
{inspectionUnavailable ? (
<AlertCircle className="mt-0.5 size-3.5 shrink-0" />
) : (
<div className="divide-y divide-border/50">
{visibleConfigs.map((config) => (
<McpConfigFileRow
key={config.candidate.relativePath}
config={config}
onOpen={handleOpen}
/>
))}
</div>
<FileCode2 className="mt-0.5 size-3.5 shrink-0" />
)}
{inspectionUnavailable ? (
<span>{inspectionUnavailableMessage}</span>
) : (
<span>
{translate(
'auto.components.settings.McpConfigSection.b900cd6282',
'No MCP config found. Add an empty workspace config when you want this repo to define its own MCP servers.'
)}
</span>
)}
{!inspectionUnavailable ? <McpMissingConfigList missingConfigs={missingConfigs} /> : null}
</div>
</div>
</section>
) : (
visibleConfigs.map((config) => (
<McpConfigFileRow
key={config.candidate.relativePath}
config={config}
onOpen={handleOpen}
/>
))
)}
{!inspectionUnavailable ? <McpMissingConfigList missingConfigs={missingConfigs} /> : null}
</SettingsGroup>
)
}
@@ -11,7 +11,7 @@ export function McpMissingConfigList({
}
return (
<div className="space-y-1.5 border-t border-border/50 px-3 py-2">
<div data-settings-row="" className="space-y-1.5 py-3">
<p className="text-[11px] text-muted-foreground">
{translate('auto.components.settings.McpConfigSection.4d16a0d9ac', 'Checked')}
</p>
@@ -118,7 +118,7 @@ const SelectedRepositoryActionRows = memo(
[selections]
)
return (
<div className="space-y-3">
<div data-settings-row="" className="space-y-3 py-3">
<Label className="text-xs font-medium">
{translate(
'auto.components.settings.RepositorySourceControlAiActionRows.f0aa2cfaea',
@@ -34,7 +34,7 @@ export function RepositorySourceControlAiCustomCommand({
const mode =
hasRepoCommand || forceRepoMode ? CUSTOM_COMMAND_MODE_REPO : CUSTOM_COMMAND_MODE_INHERIT
return (
<div className="space-y-2 rounded-md border border-border px-3 py-3">
<div data-settings-row="" className="space-y-2 py-3">
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0 space-y-0.5">
<Label className="text-xs font-medium">
@@ -32,7 +32,10 @@ export function RepositorySourceControlAiEnablement({
onChange
}: RepositorySourceControlAiEnablementProps): React.JSX.Element {
return (
<div className="flex flex-col gap-2 rounded-md border border-border px-3 py-3 sm:flex-row sm:items-center sm:justify-between">
<div
data-settings-row=""
className="flex flex-col gap-2 py-3 sm:flex-row sm:items-center sm:justify-between"
>
<div className="min-w-0 space-y-0.5">
<Label className="text-xs font-medium">
{translate(
@@ -61,7 +61,7 @@ export function RepositorySourceControlAiHostedReviewDefaults({
onChange
}: RepositorySourceControlAiHostedReviewDefaultsProps): React.JSX.Element {
return (
<div className="space-y-2">
<div data-settings-row="" className="space-y-2 py-3">
<Label className="text-xs font-medium">
{translate(
'auto.components.settings.RepositorySourceControlAiHostedReviewDefaults.aa6ee4b7d6',
@@ -15,6 +15,7 @@ import { normalizeRepoAiDraft } from './repository-source-control-ai-draft'
import { useRepositorySourceControlAiGlobalUx } from './repository-source-control-ai-global-ux'
import { getSettingOwnershipSummary } from '../../page/setting-ownership'
import { translate } from '@/i18n/i18n'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
type RepositorySourceControlAiSectionProps = {
repo: Repo
@@ -76,57 +77,58 @@ export function RepositorySourceControlAiSection({
})
return (
<section
<div
id={getRepositorySourceControlAiSectionId(repo.id)}
data-settings-section={getRepositorySourceControlAiSectionId(repo.id)}
className="space-y-4"
>
<div className="min-w-0 space-y-1">
<h3 className="text-sm font-semibold">
{translate(
'auto.components.settings.RepositorySourceControlAiSection.71b003b62b',
'Source Control AI'
)}
</h3>
<p className="text-xs text-muted-foreground">{ownership.description}</p>
{saveError ? <p className="text-xs text-destructive">{saveError}</p> : null}
</div>
<RepositorySourceControlAiEnablement
value={displayRepoAi.enabled}
source={source}
onChange={updateEnablement}
/>
<RepositorySourceControlAiCustomCommand
value={displayRepoAi.customAgentCommand}
source={source}
onChange={updateCustomCommand}
onCommit={commitCustomCommand}
/>
<RepositorySourceControlAiActionRows
repoId={repo.id}
repoAi={displayRepoAi}
actionTextDrafts={actionTextDrafts}
source={source}
defaultTuiAgent={settings?.defaultTuiAgent}
savingActionIds={savingActionIds}
actionDirtyById={actionDirtyById}
onActionModeChange={updateActionMode}
onActionAgentChange={updateActionAgent}
onActionTextModelChoice={updateActionTextModelChoice}
writeSourceControlAiSettings={writeSourceControlAiSettings}
onActionLaunchPreferencesChange={updateActionLaunchPreferences}
onActionTemplateChange={updateActionTemplate}
onActionAgentArgsChange={updateActionAgentArgs}
onAppendVariable={appendVariable}
onActionDiscard={discardActionRecipeText}
onActionSave={(actionId) => void saveActionRecipeText(actionId)}
/>
<RepositorySourceControlAiHostedReviewDefaults
value={displayRepoAi.prCreationDefaults}
source={source}
onChange={updateHostedReviewDefault}
/>
</section>
<SettingsGroup
title={translate(
'auto.components.settings.RepositorySourceControlAiSection.71b003b62b',
'Source Control AI'
)}
description={
<>
{ownership.description}
{saveError ? <span className="block text-destructive">{saveError}</span> : null}
</>
}
>
<RepositorySourceControlAiEnablement
value={displayRepoAi.enabled}
source={source}
onChange={updateEnablement}
/>
<RepositorySourceControlAiCustomCommand
value={displayRepoAi.customAgentCommand}
source={source}
onChange={updateCustomCommand}
onCommit={commitCustomCommand}
/>
<RepositorySourceControlAiActionRows
repoId={repo.id}
repoAi={displayRepoAi}
actionTextDrafts={actionTextDrafts}
source={source}
defaultTuiAgent={settings?.defaultTuiAgent}
savingActionIds={savingActionIds}
actionDirtyById={actionDirtyById}
onActionModeChange={updateActionMode}
onActionAgentChange={updateActionAgent}
onActionTextModelChoice={updateActionTextModelChoice}
writeSourceControlAiSettings={writeSourceControlAiSettings}
onActionLaunchPreferencesChange={updateActionLaunchPreferences}
onActionTemplateChange={updateActionTemplate}
onActionAgentArgsChange={updateActionAgentArgs}
onAppendVariable={appendVariable}
onActionDiscard={discardActionRecipeText}
onActionSave={(actionId) => void saveActionRecipeText(actionId)}
/>
<RepositorySourceControlAiHostedReviewDefaults
value={displayRepoAi.prCreationDefaults}
source={source}
onChange={updateHostedReviewDefault}
/>
</SettingsGroup>
</div>
)
}
@@ -86,7 +86,7 @@ export function GeneralRemoteServerUpdates(): React.JSX.Element | null {
'Check and update paired Orca servers from this client.'
)}
keywords={['remote server', 'update all', 'paired', 'version']}
className="space-y-3"
className="space-y-3 py-3"
>
<div className="space-y-0.5">
<div className="text-sm font-medium">
@@ -25,7 +25,7 @@ export function SearchableSetting({
}
return (
<div className={cn('scroll-mt-6 w-full max-w-3xl', className)} id={id}>
<div data-settings-row="" className={cn('scroll-mt-6 w-full', className)} id={id}>
{children}
</div>
)
@@ -0,0 +1,72 @@
import type React from 'react'
import { cn } from '@/lib/utils'
type SettingsGroupProps = {
title?: React.ReactNode
description?: React.ReactNode
action?: React.ReactNode
children?: React.ReactNode
className?: string
bodyClassName?: string
onHeaderClick?: (event: React.MouseEvent<HTMLDivElement>) => void
}
// Title sits outside the card so each group's start and end read at a glance.
export function SettingsGroup({
title,
description,
action,
children,
className,
bodyClassName,
onHeaderClick
}: SettingsGroupProps): React.JSX.Element {
return (
<section className={cn('space-y-3', className)}>
{title ? (
<SettingsGroupHeader
title={title}
description={description}
action={action}
onClick={onHeaderClick}
/>
) : null}
<div
className={cn(
// Search can filter out every row; hide the empty card but keep the header and its action.
'overflow-hidden rounded-xl border border-border bg-card/50 px-5 shadow-xs has-[>[data-settings-group-body]:empty]:hidden',
bodyClassName
)}
>
{/* A row inside another row (a sub-field) draws no rule; each outermost row draws one and the card clips the first. */}
<div
data-settings-group-body=""
className="-mt-px [&_[data-settings-row]:not([data-settings-group-body]_[data-settings-row]_*)]:border-t [&_[data-settings-row]:not([data-settings-group-body]_[data-settings-row]_*)]:border-t-border"
>
{children}
</div>
</div>
</section>
)
}
// Shared so lists that must keep row identity across groups can draw the same header.
export function SettingsGroupHeader({
title,
description,
action,
className,
onClick
}: Pick<SettingsGroupProps, 'title' | 'description' | 'action' | 'className'> & {
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void
}): React.JSX.Element {
return (
<div className={cn('flex items-end justify-between gap-3 px-1', className)} onClick={onClick}>
<div className="min-w-0 space-y-1">
<h3 className="text-base font-semibold leading-tight">{title}</h3>
{description ? <p className="text-xs text-muted-foreground">{description}</p> : null}
</div>
{action ? <div className="shrink-0">{action}</div> : null}
</div>
)
}
@@ -18,6 +18,8 @@ type SettingsSectionProps = {
children?: React.ReactNode
className?: string
bodyClassName?: string
/** Pane is built from SettingsGroup cards, so the body drops its own card. Fixed per pane, never query-dependent. */
grouped?: boolean
badge?: string
badgeAccessory?: React.ReactNode
forceVisible?: boolean
@@ -40,6 +42,7 @@ export function SettingsSection({
children,
className,
bodyClassName,
grouped = false,
badge,
badgeAccessory,
forceVisible = false,
@@ -86,10 +89,11 @@ export function SettingsSection({
</div>
{headerAction ? <div className="shrink-0">{headerAction}</div> : null}
</div>
{/* Contain row groups in a distinct band so they cannot read as sidebar continuations. */}
<div
className={cn(
'rounded-xl border border-border/50 bg-card/50 px-7 py-6 shadow-xs',
grouped
? 'flex flex-col gap-8'
: 'rounded-xl border border-border/50 bg-card/50 px-7 py-6 shadow-xs',
bodyClassName
)}
>
@@ -1,23 +1,10 @@
import type { ReactElement } from 'react'
import { Separator } from '../../ui/separator'
// Key by section identity, not filtered index, so query edits cannot remount surviving sections and lose drafts or repeat loads.
// Render sections by their own keys, not filtered index, so query edits cannot remount surviving sections and lose drafts or repeat loads.
export function SettingsSectionStack({
sections,
spacing
sections
}: {
sections: readonly (ReactElement | null)[]
spacing: 'section' | 'group'
}): ReactElement {
const visibleSections = sections.filter((section): section is ReactElement => section !== null)
return (
<>
{visibleSections.map((section, index) => (
<div key={section.key} className={spacing === 'group' ? 'space-y-8' : 'space-y-6'}>
{index > 0 ? <Separator /> : null}
{section}
</div>
))}
</>
)
return <div className="space-y-8">{sections.filter((section) => section !== null)}</div>
}

Some files were not shown because too many files have changed in this diff Show More