From 6f2ee281cec2f1ca6f5d47cc88890952e1dc10d3 Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 10 Nov 2026 17:37:03 -0800 Subject: [PATCH] feat(settings): Agent accounts takes parts and links them from a jump row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../accounts/AgentAccountsJumpRow.test.tsx | 44 ++++++++++++++++ .../accounts/AgentAccountsJumpRow.tsx | 51 +++++++++++++++++++ .../accounts/AgentAccountsSubsection.tsx | 25 +++++++-- .../settings-capability-section-renderers.tsx | 43 ++++++++++++---- src/renderer/src/i18n/locales/en.json | 3 +- 5 files changed, 150 insertions(+), 16 deletions(-) create mode 100644 src/renderer/src/components/settings/accounts/AgentAccountsJumpRow.test.tsx create mode 100644 src/renderer/src/components/settings/accounts/AgentAccountsJumpRow.tsx diff --git a/src/renderer/src/components/settings/accounts/AgentAccountsJumpRow.test.tsx b/src/renderer/src/components/settings/accounts/AgentAccountsJumpRow.test.tsx new file mode 100644 index 00000000000..e33cb46fc85 --- /dev/null +++ b/src/renderer/src/components/settings/accounts/AgentAccountsJumpRow.test.tsx @@ -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() + 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( + <> + + +

local

+
+ +

hosts

+
+ + ) + 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' })) + }) +}) diff --git a/src/renderer/src/components/settings/accounts/AgentAccountsJumpRow.tsx b/src/renderer/src/components/settings/accounts/AgentAccountsJumpRow.tsx new file mode 100644 index 00000000000..6a942448e93 --- /dev/null +++ b/src/renderer/src/components/settings/accounts/AgentAccountsJumpRow.tsx @@ -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 ( + + ) +} diff --git a/src/renderer/src/components/settings/accounts/AgentAccountsSubsection.tsx b/src/renderer/src/components/settings/accounts/AgentAccountsSubsection.tsx index 1b5e925067d..43200a646ad 100644 --- a/src/renderer/src/components/settings/accounts/AgentAccountsSubsection.tsx +++ b/src/renderer/src/components/settings/accounts/AgentAccountsSubsection.tsx @@ -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 (
-

+ {/* tabIndex -1: the jump row moves focus here without adding a tab stop. */} +

{title}

{description ?

{description}

: null} diff --git a/src/renderer/src/components/settings/page/settings-capability-section-renderers.tsx b/src/renderer/src/components/settings/page/settings-capability-section-renderers.tsx index 99a1f210e99..51332152ed6 100644 --- a/src/renderer/src/components/settings/page/settings-capability-section-renderers.tsx +++ b/src/renderer/src/components/settings/page/settings-capability-section-renderers.tsx @@ -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 ( {view.isSectionMounted('accounts') ? ( <> + ({ id, title })) + ]} + /> {renderProviderAccountsPane(context)} - {additionalSections} + {additionalParts.map((part) => ( + + {part.content} + + ))} ) : null} diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 8be44bd6cea..677d51a10a3 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -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": {