mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 18:37:24 +00:00
feat(settings): Agent accounts takes parts and links them from a jump row
renderAccountsSettingsSection now takes additionalParts ({ id, title, description, content }) and
titles each with AgentAccountsSubsection. When the page has more than one part, a jump row under the
description links to each one ('This computer · …'); a link scrolls the settings pane to the part and
moves focus to its heading.
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AgentAccountsJumpRow } from './AgentAccountsJumpRow'
|
||||
import { AgentAccountsSubsection } from './AgentAccountsSubsection'
|
||||
|
||||
const originalScrollIntoView = Element.prototype.scrollIntoView
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
Element.prototype.scrollIntoView = originalScrollIntoView
|
||||
})
|
||||
|
||||
describe('AgentAccountsJumpRow', () => {
|
||||
it('stays hidden when the page has only one part', () => {
|
||||
render(<AgentAccountsJumpRow links={[{ id: 'accounts-this-host', title: 'This computer' }]} />)
|
||||
expect(screen.queryByRole('navigation')).toBeNull()
|
||||
})
|
||||
|
||||
it('links every part in order and moves focus to the chosen heading', () => {
|
||||
const scrollIntoView = vi.fn()
|
||||
Element.prototype.scrollIntoView = scrollIntoView
|
||||
render(
|
||||
<>
|
||||
<AgentAccountsJumpRow
|
||||
links={[
|
||||
{ id: 'accounts-this-host', title: 'This computer' },
|
||||
{ id: 'accounts-hosts', title: 'Hosts' }
|
||||
]}
|
||||
/>
|
||||
<AgentAccountsSubsection id="accounts-this-host" title="This computer">
|
||||
<p>local</p>
|
||||
</AgentAccountsSubsection>
|
||||
<AgentAccountsSubsection id="accounts-hosts" title="Hosts">
|
||||
<p>hosts</p>
|
||||
</AgentAccountsSubsection>
|
||||
</>
|
||||
)
|
||||
const links = screen.getAllByRole('link')
|
||||
expect(links.map((link) => link.textContent)).toEqual(['This computer', 'Hosts'])
|
||||
fireEvent.click(screen.getByRole('link', { name: 'Hosts' }))
|
||||
expect(scrollIntoView).toHaveBeenCalled()
|
||||
expect(document.activeElement).toBe(screen.getByRole('heading', { name: 'Hosts' }))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
import type React from 'react'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { scrollSubsectionIntoView } from '../navigation/settings-navigation-foundations'
|
||||
import { useSettingsContentScrollElement } from '../page/settings-content-scroll-element'
|
||||
import { agentAccountsSubsectionTitleId } from './AgentAccountsSubsection'
|
||||
|
||||
export type AgentAccountsJumpLink = { id: string; title: React.ReactNode }
|
||||
|
||||
/** "This computer · Hosts · …" under the Agent accounts description, so later parts are one click
|
||||
* away from the long provider list. Each link scrolls to its part and moves focus to the heading. */
|
||||
export function AgentAccountsJumpRow({
|
||||
links
|
||||
}: {
|
||||
links: readonly AgentAccountsJumpLink[]
|
||||
}): React.JSX.Element | null {
|
||||
const t = useTranslate()
|
||||
const scroller = useSettingsContentScrollElement()
|
||||
if (links.length < 2) {
|
||||
return null
|
||||
}
|
||||
const jump = (id: string): void => {
|
||||
scrollSubsectionIntoView(id, scroller)
|
||||
document.getElementById(agentAccountsSubsectionTitleId(id))?.focus({ preventScroll: true })
|
||||
}
|
||||
return (
|
||||
<nav
|
||||
aria-label={t('settings.agentAccounts.jumpLabel', 'Parts of this page')}
|
||||
className="-mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-sm"
|
||||
>
|
||||
{links.map((link, index) => (
|
||||
<span key={link.id} className="flex items-center gap-2">
|
||||
{index > 0 ? (
|
||||
<span aria-hidden="true" className="text-muted-foreground">
|
||||
·
|
||||
</span>
|
||||
) : null}
|
||||
<a
|
||||
href={`#${link.id}`}
|
||||
className="rounded-sm text-muted-foreground underline-offset-4 hover:text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
jump(link.id)
|
||||
}}
|
||||
>
|
||||
{link.title}
|
||||
</a>
|
||||
</span>
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,18 @@
|
||||
import type React from 'react'
|
||||
|
||||
/** One part of Settings → Agent accounts beyond "This computer"; builds pass these to
|
||||
* renderAccountsSettingsSection, which titles each one and lists it in the jump row. */
|
||||
export type AgentAccountsPart = {
|
||||
id: string
|
||||
title: string
|
||||
description?: React.ReactNode
|
||||
content: React.ReactNode
|
||||
}
|
||||
|
||||
export function agentAccountsSubsectionTitleId(id: string): string {
|
||||
return `${id}-title`
|
||||
}
|
||||
|
||||
type AgentAccountsSubsectionProps = {
|
||||
id: string
|
||||
title: React.ReactNode
|
||||
@@ -7,19 +20,23 @@ type AgentAccountsSubsectionProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
/** One part of Settings → Agent accounts (this computer, other hosts, …). Builds can add their own
|
||||
* parts through renderAccountsSettingsSection's `additionalSections`. */
|
||||
/** One part of Settings → Agent accounts (this computer, other hosts, …). */
|
||||
export function AgentAccountsSubsection({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
children
|
||||
}: AgentAccountsSubsectionProps): React.JSX.Element {
|
||||
const titleId = `${id}-title`
|
||||
const titleId = agentAccountsSubsectionTitleId(id)
|
||||
return (
|
||||
<section id={id} aria-labelledby={titleId} className="scroll-mt-8 space-y-5">
|
||||
<div className="space-y-1 border-b border-border/60 pb-3">
|
||||
<h3 id={titleId} className="text-lg font-semibold leading-tight text-foreground">
|
||||
{/* tabIndex -1: the jump row moves focus here without adding a tab stop. */}
|
||||
<h3
|
||||
id={titleId}
|
||||
tabIndex={-1}
|
||||
className="text-lg font-semibold leading-tight text-foreground focus:outline-none"
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
{description ? <p className="text-sm text-muted-foreground">{description}</p> : null}
|
||||
|
||||
+32
-11
@@ -1,5 +1,9 @@
|
||||
import { AccountsPane } from '../accounts/AccountsPane'
|
||||
import { AgentAccountsSubsection } from '../accounts/AgentAccountsSubsection'
|
||||
import { AgentAccountsJumpRow } from '../accounts/AgentAccountsJumpRow'
|
||||
import {
|
||||
AgentAccountsSubsection,
|
||||
type AgentAccountsPart
|
||||
} from '../accounts/AgentAccountsSubsection'
|
||||
import { getProviderAccountsOwnerKey } from '@/runtime/provider-accounts/observations/runtime-provider-accounts-client'
|
||||
import { AgentsPane } from '../agents/AgentsPane'
|
||||
import { AgentDefaultSurfaceSection } from '../agents/surfaces/AgentDefaultSurfaceSection'
|
||||
@@ -15,6 +19,9 @@ import { translate } from '@/i18n/i18n'
|
||||
import type { Translate } from '@/i18n/use-translate'
|
||||
import type { SettingsRenderContext } from './settings-render-context'
|
||||
|
||||
/** The "This computer" / "Selected host" part of Settings → Agent accounts. */
|
||||
export const AGENT_ACCOUNTS_HOST_PART_ID = 'accounts-this-host'
|
||||
|
||||
export function renderAgentsSettingsSection(
|
||||
context: SettingsRenderContext,
|
||||
t: Translate = translate
|
||||
@@ -53,16 +60,19 @@ export function renderAgentsSettingsSection(
|
||||
|
||||
/**
|
||||
* Settings → Agent accounts: one page for agent sign-ins. "This computer" (or the host picked in the
|
||||
* scope picker) holds provider accounts, switching and usage; `additionalSections` lets a build add
|
||||
* more parts below it, each an AgentAccountsSubsection.
|
||||
* scope picker) holds provider accounts, switching and usage; `additionalParts` lets a build add
|
||||
* more parts below it. With more than one part, a jump row under the description links to each.
|
||||
*/
|
||||
export function renderAccountsSettingsSection(
|
||||
context: SettingsRenderContext,
|
||||
t: Translate = translate,
|
||||
additionalSections?: React.ReactNode
|
||||
additionalParts: readonly AgentAccountsPart[] = []
|
||||
): React.JSX.Element {
|
||||
const { model, navigation, view } = context
|
||||
const isLocalScope = model.settingsHostScope.target.kind === 'local'
|
||||
const hostPartTitle = isLocalScope
|
||||
? t('settings.agentAccounts.thisComputer', 'This computer')
|
||||
: t('settings.agentAccounts.selectedHost', 'Selected host')
|
||||
return (
|
||||
<SettingsSection
|
||||
id="accounts"
|
||||
@@ -76,13 +86,15 @@ export function renderAccountsSettingsSection(
|
||||
>
|
||||
{view.isSectionMounted('accounts') ? (
|
||||
<>
|
||||
<AgentAccountsJumpRow
|
||||
links={[
|
||||
{ id: AGENT_ACCOUNTS_HOST_PART_ID, title: hostPartTitle },
|
||||
...additionalParts.map(({ id, title }) => ({ id, title }))
|
||||
]}
|
||||
/>
|
||||
<AgentAccountsSubsection
|
||||
id="accounts-this-host"
|
||||
title={
|
||||
isLocalScope
|
||||
? t('settings.agentAccounts.thisComputer', 'This computer')
|
||||
: t('settings.agentAccounts.selectedHost', 'Selected host')
|
||||
}
|
||||
id={AGENT_ACCOUNTS_HOST_PART_ID}
|
||||
title={hostPartTitle}
|
||||
description={t(
|
||||
'auto.components.settings.Settings.21f09426ea',
|
||||
'Optional. Orca works with your existing provider logins; add accounts only if you want Orca to help switch between them.'
|
||||
@@ -90,7 +102,16 @@ export function renderAccountsSettingsSection(
|
||||
>
|
||||
{renderProviderAccountsPane(context)}
|
||||
</AgentAccountsSubsection>
|
||||
{additionalSections}
|
||||
{additionalParts.map((part) => (
|
||||
<AgentAccountsSubsection
|
||||
key={part.id}
|
||||
id={part.id}
|
||||
title={part.title}
|
||||
description={part.description}
|
||||
>
|
||||
{part.content}
|
||||
</AgentAccountsSubsection>
|
||||
))}
|
||||
</>
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
|
||||
@@ -1346,7 +1346,8 @@
|
||||
"agentAccounts": {
|
||||
"pageDescription": "Sign-ins, account switching and usage for your agents.",
|
||||
"thisComputer": "This computer",
|
||||
"selectedHost": "Selected host"
|
||||
"selectedHost": "Selected host",
|
||||
"jumpLabel": "Parts of this page"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
|
||||
Reference in New Issue
Block a user