mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 02:17:31 +00:00
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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+293
-288
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+252
-236
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+202
-178
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+166
-165
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+58
-52
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+188
-187
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+4
-3
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+274
-277
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+2
-2
@@ -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">
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+1
-1
@@ -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>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+25
-25
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+48
-45
@@ -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>
|
||||
|
||||
+1
-1
@@ -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">
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+37
-45
@@ -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'
|
||||
|
||||
+21
-22
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+13
-20
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+80
-77
@@ -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',
|
||||
|
||||
+3
@@ -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',
|
||||
|
||||
+1
@@ -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}}',
|
||||
|
||||
+1
@@ -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',
|
||||
|
||||
+7
@@ -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>
|
||||
|
||||
+1
-1
@@ -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">
|
||||
|
||||
+161
-151
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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">
|
||||
|
||||
+3
-3
@@ -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">
|
||||
|
||||
+101
-102
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+149
-150
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+1
-1
@@ -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',
|
||||
|
||||
+1
-1
@@ -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">
|
||||
|
||||
+4
-1
@@ -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(
|
||||
|
||||
+1
-1
@@ -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',
|
||||
|
||||
+51
-49
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
Reference in New Issue
Block a user