diff --git a/src/renderer/src/components/settings/AppearanceChatSection.test.tsx b/src/renderer/src/components/settings/AppearanceChatSection.test.tsx
index 7ad07917e96..ba53111824c 100644
--- a/src/renderer/src/components/settings/AppearanceChatSection.test.tsx
+++ b/src/renderer/src/components/settings/AppearanceChatSection.test.tsx
@@ -5,8 +5,10 @@ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/re
import { afterEach, describe, expect, it, vi } from 'vitest'
import { getDefaultSettings } from '../../../../shared/constants'
import { AppearanceChatSection } from './AppearanceChatSection'
-import { getChatAppearanceEntriesByKey } from './chat-appearance-search'
-import { getAppearancePaneSearchEntries } from './appearance-search'
+import {
+ getChatAppearanceEntriesByKey,
+ getChatAppearanceSearchEntries
+} from './chat-appearance-search'
import { matchesSettingsSearch } from './settings-search'
const mocks = vi.hoisted(
@@ -46,7 +48,7 @@ function persistInMock(settings: GlobalSettings) {
})
}
-describe('chat appearance settings card', () => {
+describe('chat appearance settings controls', () => {
it.each([
{ platform: 'darwin', increase: '⌘=', decrease: '⌘-' },
{ platform: 'win32', increase: 'Ctrl+=', decrease: 'Ctrl+-' },
@@ -142,8 +144,8 @@ describe('chat appearance settings card', () => {
await waitFor(() => expect(updateSettings).toHaveBeenCalledTimes(2))
expect(mocks.state.settings?.nativeChatAppearance).toEqual({ contrast: 151 })
})
- it('indexes each row and width choice in Appearance settings search', () => {
- const entries = getAppearancePaneSearchEntries()
+ it('indexes each row and width choice in Chat settings search', () => {
+ const entries = getChatAppearanceSearchEntries()
for (const query of [
'Chat',
'Code text size',
diff --git a/src/renderer/src/components/settings/AppearanceChatSection.tsx b/src/renderer/src/components/settings/AppearanceChatSection.tsx
index ccd629151a6..067b0768cb7 100644
--- a/src/renderer/src/components/settings/AppearanceChatSection.tsx
+++ b/src/renderer/src/components/settings/AppearanceChatSection.tsx
@@ -38,7 +38,11 @@ export function AppearanceChatSection({
}
return (
-
+
update({ fontSize })}
/>
-
+
update({ codeFontSize })}
/>
-
+
-
+
{
delete (window as unknown as { api?: unknown }).api
})
+ it('keeps chat appearance controls out of the Appearance pane', async () => {
+ mocks.state.settingsSearchQuery = ''
+ const container = await renderAppearancePane({
+ ...getDefaultSettings('/tmp'),
+ experimentalStructuredNativeChat: true
+ })
+ expect(appearanceSectionToggle(container, 'chat')).toBeUndefined()
+ expect(container.textContent).not.toContain('Reset chat appearance')
+ })
+
it('shows language as a primary interface control without opening Advanced', async () => {
mocks.state.settingsSearchQuery = ''
const container = await renderAppearancePane(getDefaultSettings('/tmp'))
@@ -397,7 +407,7 @@ describe('AppearancePane', () => {
expect(mocks.state.setWorktreeCardMode).toHaveBeenCalledWith('Compact')
})
- it('renders the four top-level section rows and no Code & Markdown row when not searching', async () => {
+ it('renders the three top-level section rows and no Code & Markdown row when not searching', async () => {
mocks.state.settingsSearchQuery = ''
const container = await renderAppearancePane(getDefaultSettings('/tmp'))
@@ -580,7 +590,7 @@ describe('AppearancePane', () => {
expect(mocks.state.toggleStatusBarItem).toHaveBeenCalledWith('antigravity')
})
- it('expands Interface, Terminal, Chat, and Window & Sidebar by default', async () => {
+ it('expands Interface, Terminal, and Window & Sidebar by default', async () => {
mocks.state.settingsSearchQuery = ''
const container = await renderAppearancePane(getDefaultSettings('/tmp'))
@@ -588,7 +598,7 @@ describe('AppearancePane', () => {
container.querySelectorAll('button[aria-expanded="true"]')
).filter((button) => button.getAttribute('aria-controls')?.startsWith('appearance-section-'))
- expect(expanded).toHaveLength(4)
+ expect(expanded).toHaveLength(3)
expect(expanded.map((button) => button.textContent).join(' ')).toContain('Interface')
expect(expanded.map((button) => button.textContent).join(' ')).toContain('Terminal')
expect(expanded.map((button) => button.textContent).join(' ')).toContain('Window & Sidebar')
@@ -612,7 +622,7 @@ describe('AppearancePane', () => {
container.querySelectorAll('button[aria-expanded="true"]')
).filter((button) => button.getAttribute('aria-controls')?.startsWith('appearance-section-'))
- expect(stillExpanded).toHaveLength(3)
+ expect(stillExpanded).toHaveLength(2)
expect(stillExpanded.map((button) => button.textContent).join(' ')).toContain('Interface')
expect(stillExpanded.map((button) => button.textContent).join(' ')).toContain(
'Window & Sidebar'
diff --git a/src/renderer/src/components/settings/AppearancePane.tsx b/src/renderer/src/components/settings/AppearancePane.tsx
index 9ef0888fb7f..f6fd1b426ee 100644
--- a/src/renderer/src/components/settings/AppearancePane.tsx
+++ b/src/renderer/src/components/settings/AppearancePane.tsx
@@ -1,12 +1,9 @@
-import { resolveNativeChatAppearanceSettings } from '../../../../shared/native-chat-appearance-settings'
-import { getChatAppearanceSearchEntries, getChatWidthOptions } from './chat-appearance-search'
import type React from 'react'
import { useLayoutEffect, useState } from 'react'
-import { AppWindow, MessageSquare, PanelLeft, TerminalSquare } from 'lucide-react'
+import { AppWindow, PanelLeft, TerminalSquare } from 'lucide-react'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
-import { AppearanceChatSection } from './AppearanceChatSection'
import { AppearanceSection } from './AppearanceSection'
import { AppearanceInterfaceSection } from './AppearanceInterfaceSection'
import { AppearanceWindowSidebarSection } from './AppearanceWindowSidebarSection'
@@ -57,12 +54,11 @@ type AppearancePaneProps = {
warpThemes: UseWarpThemeImportReturn
}
-type AppearanceSectionKey = 'interface' | 'terminal' | 'chat' | 'window'
+type AppearanceSectionKey = 'interface' | 'terminal' | 'window'
const ALL_APPEARANCE_SECTIONS = [
'interface',
'terminal',
- 'chat',
'window'
] as const satisfies readonly AppearanceSectionKey[]
@@ -129,21 +125,6 @@ export function AppearancePane({
'auto.components.settings.AppearancePane.terminalTitle',
'Terminal'
)
- const chatTitle = translate('settings.appearance.chat.title', 'Chat')
- const chatMatches = matchesSettingsSearch(searchQuery, [
- { title: chatTitle },
- ...getChatAppearanceSearchEntries()
- ])
- const chatLabelMatches = matchesSettingsSearch(searchQuery, { title: chatTitle })
- const chatAppearance = resolveNativeChatAppearanceSettings(settings.nativeChatAppearance)
- const chatSummary = translate(
- 'settings.appearance.chat.summary',
- '{{fontSize}}px · {{width}} width',
- {
- fontSize: chatAppearance.fontSize,
- width: getChatWidthOptions().find((option) => option.value === chatAppearance.width)?.label
- }
- )
const windowSidebarTitle = translate(
'auto.components.settings.AppearancePane.windowSidebarTitle',
'Window & Sidebar'
@@ -202,9 +183,7 @@ export function AppearancePane({
? interfaceMatches
: key === 'terminal'
? terminalMatches
- : key === 'chat'
- ? chatMatches
- : windowMatches
+ : windowMatches
}
return openSections.has(key)
}
@@ -275,24 +254,6 @@ export function AppearancePane({
) : null}
- {chatMatches ? (
- }
- title={chatTitle}
- summary={chatSummary}
- open={isSectionOpen('chat')}
- onToggle={() => toggleSection('chat')}
- toggleDisabled={isSearching}
- >
-
-
- ) : null}
-
{windowMatches ? (
({
+ useAppStore: (selector: (value: { settingsSearchQuery: string }) => unknown) =>
+ selector({ settingsSearchQuery: '' })
+}))
+afterEach(cleanup)
+
+function NavigationHarness({
+ enabled,
+ initialSection
+}: {
+ enabled: boolean
+ initialSection: string
+}) {
+ const [activeSectionId, setActiveSectionId] = useState(initialSection)
+ const settings = { ...getDefaultSettings('/tmp'), experimentalStructuredNativeChat: enabled }
+ const sections = buildSettingsNavigationMetadata({
+ isMac: false,
+ isWindows: false,
+ isWebClient: false,
+ experimentalStructuredNativeChat: enabled,
+ repos: []
+ })
+ const pendingNavSectionRef = useRef('chat')
+ const pendingScrollTargetRef = useRef('chat-code-text-size')
+ const contentScrollRef = useRef(null)
+ const pendingScrollTargetWatchRef = useRef(null)
+ const pendingSubsectionScrollFrameRef = useRef(null)
+ const repoHooksRequestSeqRef = useRef(0)
+ const model: Pick<
+ SettingsStoreModel,
+ | 'activeSectionId'
+ | 'setActiveSectionId'
+ | 'pendingNavRequestTick'
+ | 'setPendingNavRequestTick'
+ | 'repos'
+ | 'setRepoHooksMap'
+ | 'settingsSearchQuery'
+ | 'setSettingsSearchQuery'
+ > = {
+ activeSectionId,
+ setActiveSectionId,
+ pendingNavRequestTick: 0,
+ setPendingNavRequestTick: vi.fn(),
+ repos: [],
+ setRepoHooksMap: vi.fn(),
+ settingsSearchQuery: '',
+ setSettingsSearchQuery: vi.fn()
+ }
+ const interactions = {
+ contentScrollRef,
+ pendingNavSectionRef,
+ pendingScrollTargetRef,
+ pendingScrollTargetWatchRef,
+ pendingSubsectionScrollFrameRef,
+ repoHooksRequestSeqRef
+ }
+ const terminal: Pick = { neededRepos: [] }
+ useSettingsRepoScrollEffects(
+ // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Supplies every model field read by this hook; no repos need loading.
+ model as SettingsStoreModel,
+ // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Supplies every interaction ref read by this hook.
+ interactions as SettingsInteractionController,
+ // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This hook only reads the visible navigation sections and IDs.
+ {
+ visibleNavSections: sections,
+ visibleSectionIds: new Set(sections.map((s) => s.id))
+ } as SettingsNavigationModel,
+ // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This hook only reads neededRepos; an empty list skips remote loading.
+ terminal as SettingsTerminalModel
+ )
+ return (
+
+
+
+
+ )
+}
+
+describe('Chat settings deep links', () => {
+ it('activates Chat and renders the moved row for a deep link', async () => {
+ render()
+ await waitFor(() => {
+ expect(screen.getByRole('status', { name: 'Selected settings page' }).textContent).toBe(
+ 'chat'
+ )
+ })
+ expect(screen.getByRole('spinbutton', { name: 'Code text size' })).toBeTruthy()
+ })
+
+ it('falls back through the existing navigation rule when a hidden Chat page is selected', async () => {
+ const { container } = render()
+ await waitFor(() => {
+ expect(screen.getByRole('status', { name: 'Selected settings page' }).textContent).toBe(
+ 'agents'
+ )
+ })
+ expect(container.querySelector('#chat')).toBeNull()
+ })
+
+ it('keeps the current visible page for a deep link to hidden Chat', () => {
+ const { container } = render()
+ expect(screen.getByRole('status', { name: 'Selected settings page' }).textContent).toBe(
+ 'appearance'
+ )
+ expect(container.querySelector('#chat')).toBeNull()
+ })
+})
diff --git a/src/renderer/src/components/settings/ChatSettingsSection.test.tsx b/src/renderer/src/components/settings/ChatSettingsSection.test.tsx
new file mode 100644
index 00000000000..9299cfca1d3
--- /dev/null
+++ b/src/renderer/src/components/settings/ChatSettingsSection.test.tsx
@@ -0,0 +1,147 @@
+// @vitest-environment happy-dom
+import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { getDefaultSettings } from '../../../../shared/constants'
+import type { GlobalSettings } from '../../../../shared/global-settings-types'
+import { ChatSettingsSection } from './ChatSettingsSection'
+import { ActiveSettingsSectionProvider } from './SettingsSection'
+import { getChatAppearanceSearchEntries } from './chat-appearance-search'
+import { buildSettingsNavigationMetadata } from '@/hooks/useSettingsNavigationMetadata'
+import { buildCmdJSettingsResults } from '../cmd-j/palette-results'
+import { isSettingsNavigationTarget } from '@/lib/settings-navigation-types'
+import { getSettingsSectionId, getSettingsScrollTarget } from './settings-navigation-foundations'
+
+const state = vi.hoisted((): { settingsSearchQuery: string; settings: GlobalSettings | null } => ({
+ settingsSearchQuery: '',
+ settings: null
+}))
+vi.mock('../../store', () => ({
+ useAppStore: Object.assign((selector: (value: typeof state) => unknown) => selector(state), {
+ getState: () => state
+ })
+}))
+
+afterEach(cleanup)
+beforeEach(() => {
+ state.settingsSearchQuery = ''
+ state.settings = null
+})
+
+function renderChat(enabled: boolean | undefined) {
+ const settings = { ...getDefaultSettings('/tmp'), experimentalStructuredNativeChat: enabled }
+ state.settings = settings
+ const updateSettings = vi.fn(async (updates: Partial) => {
+ if (state.settings) {
+ state.settings = { ...state.settings, ...updates }
+ }
+ })
+ const element = (active = 'chat') => (
+
+
+
+ )
+ return { ...render(element()), element, updateSettings }
+}
+
+describe('Chat settings page', () => {
+ it.each([false, undefined])('is absent with structured chat set to %s', (enabled) => {
+ const { container } = renderChat(enabled)
+ expect(container.querySelector('#chat')).toBeNull()
+ })
+
+ it('renders the existing controls under Appearance and writes the same settings', async () => {
+ const { container, updateSettings } = renderChat(true)
+ expect(screen.getByRole('heading', { name: 'Chat', level: 2 })).toBeTruthy()
+ expect(screen.getByRole('heading', { name: 'Appearance', level: 3 })).toBeTruthy()
+ expect(container.querySelector('button[aria-controls="appearance-section-chat"]')).toBeNull()
+ expect(screen.getByRole('spinbutton', { name: 'Text size' }).getAttribute('value')).toBe('14')
+ expect(screen.getByRole('spinbutton', { name: 'Code text size' }).getAttribute('value')).toBe(
+ '12'
+ )
+ expect(screen.getByRole('radio', { name: 'Comfortable' })).toBeTruthy()
+ fireEvent.click(screen.getByRole('radio', { name: 'Wide' }))
+ await waitFor(() =>
+ expect(updateSettings).toHaveBeenLastCalledWith({ nativeChatAppearance: { width: 'wide' } })
+ )
+ fireEvent.click(screen.getByRole('button', { name: 'Reset' }))
+ await waitFor(() =>
+ expect(updateSettings).toHaveBeenLastCalledWith({ nativeChatAppearance: undefined })
+ )
+ })
+
+ it('unmounts the page when the opt-in is disabled while it is selected', () => {
+ const { container, rerender } = renderChat(true)
+ rerender(
+
+
+
+ )
+ expect(container.querySelector('#chat')).toBeNull()
+ })
+
+ it.each(['Chat', 'Appearance'])('shows every row for the %s heading search', (query) => {
+ state.settingsSearchQuery = query
+ renderChat(true)
+ expect(screen.getByRole('spinbutton', { name: 'Text size' })).toBeTruthy()
+ expect(screen.getByRole('spinbutton', { name: 'Code text size' })).toBeTruthy()
+ expect(screen.getByRole('button', { name: 'Reset' })).toBeTruthy()
+ })
+
+ it('indexes Appearance on Chat without adding ambiguous palette rows', () => {
+ const sections = buildSettingsNavigationMetadata({
+ isMac: true,
+ isWindows: false,
+ isWebClient: false,
+ experimentalStructuredNativeChat: true,
+ repos: []
+ })
+ const results = buildCmdJSettingsResults(sections)
+ expect(results.filter((entry) => entry.title === 'Appearance')).toHaveLength(1)
+ const chatResults = results.filter((entry) => entry.sectionId === 'chat')
+ expect(chatResults.map((entry) => entry.title)).not.toContain('Appearance')
+ expect(chatResults.find((entry) => !entry.targetSectionId)?.configKeywords).toEqual(
+ expect.arrayContaining(['appearance'])
+ )
+ })
+
+ it('searches a moved row and resolves its deep link within the Chat page', () => {
+ state.settingsSearchQuery = 'Code text size'
+ const { container, element, rerender } = renderChat(true)
+ expect(screen.queryByRole('spinbutton', { name: 'Text size' })).toBeNull()
+ expect(screen.getByRole('spinbutton', { name: 'Code text size' })).toBeTruthy()
+ const sections = buildSettingsNavigationMetadata({
+ isMac: true,
+ isWindows: false,
+ isWebClient: false,
+ experimentalStructuredNativeChat: true,
+ repos: []
+ })
+ const result = buildCmdJSettingsResults(sections).find(
+ (entry) => entry.sectionId === 'chat' && entry.title === 'Code text size'
+ )
+ expect(result?.targetSectionId).toBe('chat-code-text-size')
+ const target = { pane: 'chat', repoId: null, sectionId: result?.targetSectionId } as const
+ expect(isSettingsNavigationTarget(target)).toBe(true)
+ expect(getSettingsSectionId(target.pane, target.repoId, new Map())).toBe('chat')
+ expect(getSettingsScrollTarget(target.sectionId ?? '', container)?.querySelector('input')).toBe(
+ screen.getByRole('spinbutton', { name: 'Code text size' })
+ )
+ rerender(element('appearance'))
+ expect(container.querySelector('#chat')).toBeNull()
+ state.settingsSearchQuery = ''
+ rerender(element())
+ for (const entry of getChatAppearanceSearchEntries().filter((entry) => entry.targetSectionId)) {
+ expect(getSettingsScrollTarget(entry.targetSectionId ?? '', container)).toBeTruthy()
+ }
+ })
+})
diff --git a/src/renderer/src/components/settings/ChatSettingsSection.tsx b/src/renderer/src/components/settings/ChatSettingsSection.tsx
new file mode 100644
index 00000000000..5ab9d60e055
--- /dev/null
+++ b/src/renderer/src/components/settings/ChatSettingsSection.tsx
@@ -0,0 +1,49 @@
+import { translate } from '@/i18n/i18n'
+import { AppearanceChatSection } from './AppearanceChatSection'
+import { SettingsSection } from './SettingsSection'
+import { SettingsSubsectionHeader } from './SettingsFormControls'
+import { matchesSettingsSearch } from './settings-search'
+import type { GlobalSettings } from '../../../../shared/global-settings-types'
+import type { SettingsSearchEntry } from './settings-search'
+import { useAppStore } from '../../store'
+
+export function ChatSettingsSection({
+ settings,
+ updateSettings,
+ searchEntries,
+ isMounted
+}: {
+ settings: GlobalSettings
+ updateSettings: (updates: Partial) => void
+ searchEntries: SettingsSearchEntry[]
+ isMounted: boolean
+}): React.JSX.Element | null {
+ const query = useAppStore((state) => state.settingsSearchQuery)
+ if (settings.experimentalStructuredNativeChat !== true) {
+ return null
+ }
+ const title = translate('settings.appearance.chat.title', 'Chat')
+ const appearanceTitle = translate('auto.components.settings.Settings.2b4474780a', 'Appearance')
+ return (
+
+ {isMounted ? (
+
+ ) : null}
+
+ )
+}
diff --git a/src/renderer/src/components/settings/SettingsSidebar.test.tsx b/src/renderer/src/components/settings/SettingsSidebar.test.tsx
index b852fe6cfe3..8d7a12ef555 100644
--- a/src/renderer/src/components/settings/SettingsSidebar.test.tsx
+++ b/src/renderer/src/components/settings/SettingsSidebar.test.tsx
@@ -4,6 +4,8 @@ import { renderToStaticMarkup } from 'react-dom/server'
import { Bot, GitBranch, Mic, Network, Puzzle } from 'lucide-react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { getDefaultSettings } from '../../../../shared/constants'
+import { buildSettingsNavigationMetadata } from '@/hooks/useSettingsNavigationMetadata'
+import type * as ShortcutLabels from '@/hooks/useShortcutLabel'
import { SettingsSidebar } from './SettingsSidebar'
import { TooltipProvider } from '../ui/tooltip'
import type { SettingsSetupGuideProgress } from './settings-setup-guide-progress'
@@ -13,10 +15,14 @@ const mocks = vi.hoisted(() => ({
useSettingsSetupGuideProgress: vi.fn()
}))
-vi.mock('@/hooks/useShortcutLabel', () => ({
- useShortcutLabel: () => '⌘F',
- useShortcutKeyComboDetails: () => [{ keys: ['⌘', 'F'], doubleTap: false }]
-}))
+vi.mock('@/hooks/useShortcutLabel', async () => {
+ const actual = await vi.importActual('@/hooks/useShortcutLabel')
+ return {
+ ...actual,
+ useShortcutLabel: () => '⌘F',
+ useShortcutKeyComboDetails: () => [{ keys: ['⌘', 'F'], doubleTap: false }]
+ }
+})
vi.mock('./settings-setup-guide-progress', () => ({
useSettingsSetupGuideProgress: mocks.useSettingsSetupGuideProgress
@@ -128,6 +134,33 @@ describe('SettingsSidebar', () => {
document.body.innerHTML = ''
})
+ it.each([false, true])('renders Chat navigation only with the opt-in enabled (%s)', (enabled) => {
+ const settings = { ...getDefaultSettings('/tmp'), experimentalStructuredNativeChat: enabled }
+ const sections = buildSettingsNavigationMetadata({
+ isMac: false,
+ isWindows: false,
+ isWebClient: false,
+ experimentalStructuredNativeChat: enabled,
+ repos: []
+ }).filter((section) => section.group === 'interface')
+ const container = document.createElement('div')
+ container.innerHTML = renderToStaticMarkup(
+
+
+
+ )
+ const buttons = Array.from(container.querySelectorAll('button'))
+ expect(buttons.some((button) => button.textContent === 'Chat')).toBe(enabled)
+ })
+
it('applies left sidebar appearance styles to the settings navigation', () => {
const markup = renderSidebar('orchestration', {
...getDefaultSettings('/tmp'),
diff --git a/src/renderer/src/components/settings/appearance-search.ts b/src/renderer/src/components/settings/appearance-search.ts
index 549a644fa5e..3f01e73ab35 100644
--- a/src/renderer/src/components/settings/appearance-search.ts
+++ b/src/renderer/src/components/settings/appearance-search.ts
@@ -1,4 +1,3 @@
-import { getChatAppearanceSearchEntries } from './chat-appearance-search'
import type { SettingsSearchEntry } from './settings-search'
import { getTerminalAppearanceSearchEntries } from './terminal-search'
import { getLeftSidebarAppearanceEntry, getSidebarEntries } from './appearance-sidebar-search'
@@ -258,8 +257,6 @@ export function getAppearancePaneSearchEntries(
): SettingsSearchEntry[] {
return [
...getAppearanceSectionEntries(),
- { title: translate('settings.appearance.chat.title', 'Chat') },
- ...getChatAppearanceSearchEntries(),
...getThemeEntries(),
...(SHOW_UI_LANGUAGE_SETTING ? getLanguageEntries() : []),
...getTypographyEntries(),
diff --git a/src/renderer/src/components/settings/chat-appearance-search.ts b/src/renderer/src/components/settings/chat-appearance-search.ts
index 134840d029d..a35a08aa912 100644
--- a/src/renderer/src/components/settings/chat-appearance-search.ts
+++ b/src/renderer/src/components/settings/chat-appearance-search.ts
@@ -7,10 +7,12 @@ const getChatAppearanceCatalog = createLocalizedCatalog(
() =>
({
textSize: {
+ targetSectionId: 'chat-text-size',
title: translate('settings.appearance.chat.textSize', 'Text size'),
keywords: [translate('settings.appearance.chat.title', 'Chat')]
},
codeTextSize: {
+ targetSectionId: 'chat-code-text-size',
title: translate('settings.appearance.chat.codeTextSize', 'Code text size'),
description: translate(
'settings.appearance.chat.codeTextSizeDescription',
@@ -19,6 +21,7 @@ const getChatAppearanceCatalog = createLocalizedCatalog(
keywords: [translate('settings.appearance.chat.title', 'Chat')]
},
width: {
+ targetSectionId: 'chat-width',
title: translate('settings.appearance.chat.width', 'Width'),
description: translate(
'settings.appearance.chat.widthDescription',
@@ -30,6 +33,7 @@ const getChatAppearanceCatalog = createLocalizedCatalog(
]
},
reset: {
+ targetSectionId: 'chat-reset',
title: translate('settings.appearance.chat.resetAppearance', 'Reset chat appearance'),
description: translate(
'settings.appearance.chat.resetDescription',
@@ -58,7 +62,12 @@ export function getChatAppearanceEntriesByKey(shortcuts?: { increase: string; de
}
export function getChatAppearanceSearchEntries(): SettingsSearchEntry[] {
- return Object.values(getChatAppearanceEntriesByKey())
+ return [
+ {
+ title: translate('auto.components.settings.Settings.2b4474780a', 'Appearance')
+ },
+ ...Object.values(getChatAppearanceEntriesByKey())
+ ]
}
export function getChatWidthOptions() {
diff --git a/src/renderer/src/components/settings/settings-interface-secondary-section-renderers.tsx b/src/renderer/src/components/settings/settings-interface-secondary-section-renderers.tsx
index 2a5b710247b..8643be591df 100644
--- a/src/renderer/src/components/settings/settings-interface-secondary-section-renderers.tsx
+++ b/src/renderer/src/components/settings/settings-interface-secondary-section-renderers.tsx
@@ -1,5 +1,6 @@
import { StatsPane } from '../stats/StatsPane'
import { AppearancePane } from './AppearancePane'
+import { ChatSettingsSection } from './ChatSettingsSection'
import { InputPane } from './InputPane'
import { NotificationsPane } from './NotificationsPane'
import { ShortcutsPane } from './ShortcutsPane'
@@ -36,6 +37,18 @@ export function renderAppearanceSettingsSection(context: SettingsRenderContext):
)
}
+export function renderChatSettingsSection(context: SettingsRenderContext): React.JSX.Element {
+ const { model, navigation, view } = context
+ return (
+
+ )
+}
+
export function renderInputSettingsSection(context: SettingsRenderContext): React.JSX.Element {
const { model, navigation } = context
return (
diff --git a/src/renderer/src/components/settings/settings-page-renderer.tsx b/src/renderer/src/components/settings/settings-page-renderer.tsx
index 4fb00229072..809cad1b78c 100644
--- a/src/renderer/src/components/settings/settings-page-renderer.tsx
+++ b/src/renderer/src/components/settings/settings-page-renderer.tsx
@@ -35,6 +35,7 @@ import {
} from './settings-interface-primary-section-renderers'
import {
renderAppearanceSettingsSection,
+ renderChatSettingsSection,
renderInputSettingsSection,
renderNotificationsSettingsSection,
renderShortcutsSettingsSection,
@@ -137,6 +138,7 @@ export function renderSettingsPage(context: SettingsRenderContext): React.JSX.El
{renderMobileEmulatorSettingsSection(context)}
{renderFloatingWorkspaceSettingsSection(context)}
{renderAppearanceSettingsSection(context)}
+ {renderChatSettingsSection(context)}
{renderInputSettingsSection(context)}
{renderNotificationsSettingsSection(context)}
{renderShortcutsSettingsSection(context)}
diff --git a/src/renderer/src/hooks/settings-navigation-build-options.ts b/src/renderer/src/hooks/settings-navigation-build-options.ts
index b11d040a65b..9ea40e53593 100644
--- a/src/renderer/src/hooks/settings-navigation-build-options.ts
+++ b/src/renderer/src/hooks/settings-navigation-build-options.ts
@@ -7,6 +7,7 @@ export type SettingsNavigationBuildOptions = {
isLocalWindowsHost: boolean
isWindowsTerminalHost: boolean
isWebClient: boolean
+ experimentalStructuredNativeChat: boolean
managedBrowserCreationEnabled: boolean
mobileEmulatorCreationEnabled: boolean
isDev: boolean
diff --git a/src/renderer/src/hooks/settings-navigation-interface-sections.ts b/src/renderer/src/hooks/settings-navigation-interface-sections.ts
index 015a8c9c686..3607cf4e394 100644
--- a/src/renderer/src/hooks/settings-navigation-interface-sections.ts
+++ b/src/renderer/src/hooks/settings-navigation-interface-sections.ts
@@ -1,17 +1,19 @@
import { getAppearancePaneSearchEntries } from '@/components/settings/appearance-search'
+import { getChatAppearanceSearchEntries } from '@/components/settings/chat-appearance-search'
import { getInputPaneSearchEntries } from '@/components/settings/input-search'
import { getNotificationsPaneSearchEntries } from '@/components/settings/notifications-search'
import { getShortcutsPaneSearchEntries } from '@/components/settings/shortcuts-search'
import { getStatsPaneSearchEntries } from '@/components/stats/stats-search'
import { translate } from '@/i18n/i18n'
import type { SettingsNavSection } from '@/lib/settings-navigation-types'
-import { BarChart3, Bell, Keyboard, Palette, TextCursorInput } from 'lucide-react'
+import { BarChart3, Bell, Keyboard, MessageSquare, Palette, TextCursorInput } from 'lucide-react'
import type { SettingsNavigationBuildOptions } from './settings-navigation-build-options'
export function buildInterfaceSettingsSections({
isMac,
isWindows,
isWebClient,
+ experimentalStructuredNativeChat,
managedBrowserCreationEnabled,
mobileEmulatorCreationEnabled
}: SettingsNavigationBuildOptions): SettingsNavSection[] {
@@ -32,6 +34,18 @@ export function buildInterfaceSettingsSections({
}),
group: 'interface'
},
+ ...(experimentalStructuredNativeChat
+ ? [
+ {
+ id: 'chat',
+ title: translate('settings.appearance.chat.title', 'Chat'),
+ description: translate('settings.chat.description', 'Choose how chats look.'),
+ icon: MessageSquare,
+ searchEntries: getChatAppearanceSearchEntries(),
+ group: 'interface'
+ }
+ ]
+ : []),
{
id: 'input',
title: translate('auto.hooks.useSettingsNavigationMetadata.0c6ee88a5f', 'Input & Editing'),
diff --git a/src/renderer/src/hooks/useSettingsNavigationMetadata.test.ts b/src/renderer/src/hooks/useSettingsNavigationMetadata.test.ts
index 00db574c5e0..1f8df823650 100644
--- a/src/renderer/src/hooks/useSettingsNavigationMetadata.test.ts
+++ b/src/renderer/src/hooks/useSettingsNavigationMetadata.test.ts
@@ -1,3 +1,5 @@
+import { buildCmdJSettingsResults } from '../components/cmd-j/palette-results'
+import { matchesSettingsSearch } from '../components/settings/settings-search'
import { describe, expect, it } from 'vitest'
import { buildSettingsNavigationMetadata } from './useSettingsNavigationMetadata'
import type { Repo } from '../../../shared/repo-types'
@@ -31,6 +33,47 @@ function ids(
}
describe('settings navigation metadata', () => {
+ it.each([false, undefined])(
+ 'omits Chat navigation and search when the opt-in is %s',
+ (enabled) => {
+ const sections = buildSettingsNavigationMetadata({
+ isMac: false,
+ isWindows: false,
+ isWebClient: false,
+ experimentalStructuredNativeChat: enabled,
+ repos: []
+ })
+ expect(sections.some((section) => section.id === 'chat')).toBe(false)
+ expect(buildCmdJSettingsResults(sections).some((result) => result.sectionId === 'chat')).toBe(
+ false
+ )
+ for (const query of ['Code text size', 'Reset chat appearance']) {
+ expect(
+ sections.some((section) => matchesSettingsSearch(query, section.searchEntries))
+ ).toBe(false)
+ }
+ }
+ )
+
+ it('places Chat directly after Appearance in Interface and moves its search entries', () => {
+ const sections = buildSettingsNavigationMetadata({
+ isMac: false,
+ isWindows: false,
+ isWebClient: false,
+ experimentalStructuredNativeChat: true,
+ repos: []
+ })
+ const appearanceIndex = sections.findIndex((section) => section.id === 'appearance')
+ const chat = sections[appearanceIndex + 1]
+ expect(chat.id).toBe('chat')
+ expect(chat.title).toBe('Chat')
+ expect(chat.group).toBe('interface')
+ for (const query of ['Code text size', 'Reset chat appearance']) {
+ expect(matchesSettingsSearch(query, chat.searchEntries)).toBe(true)
+ expect(matchesSettingsSearch(query, sections[appearanceIndex].searchEntries)).toBe(false)
+ }
+ })
+
it('puts AI capability panes at the top on desktop', () => {
expect(ids().slice(0, 10)).toEqual([
'agents',
diff --git a/src/renderer/src/hooks/useSettingsNavigationMetadata.ts b/src/renderer/src/hooks/useSettingsNavigationMetadata.ts
index 494e2ec613c..e7ae73e1b2a 100644
--- a/src/renderer/src/hooks/useSettingsNavigationMetadata.ts
+++ b/src/renderer/src/hooks/useSettingsNavigationMetadata.ts
@@ -41,6 +41,7 @@ export function buildSettingsNavigationMetadata({
isLocalWindowsHost = isWindows,
isWindowsTerminalHost = isWindows,
isWebClient,
+ experimentalStructuredNativeChat = false,
managedBrowserCreationEnabled = !isWebClient,
mobileEmulatorCreationEnabled = !isWebClient,
isDev = import.meta.env.DEV,
@@ -53,6 +54,7 @@ export function buildSettingsNavigationMetadata({
isLocalWindowsHost?: boolean
isWindowsTerminalHost?: boolean
isWebClient: boolean
+ experimentalStructuredNativeChat?: boolean
managedBrowserCreationEnabled?: boolean
mobileEmulatorCreationEnabled?: boolean
isDev?: boolean
@@ -80,6 +82,7 @@ export function buildSettingsNavigationMetadata({
isLocalWindowsHost,
isWindowsTerminalHost,
isWebClient,
+ experimentalStructuredNativeChat,
managedBrowserCreationEnabled,
mobileEmulatorCreationEnabled,
isDev,
@@ -119,6 +122,7 @@ export function useSettingsNavigationMetadata(): SettingsNavSection[] {
] as const
})
)
+ const experimentalStructuredNativeChat = settings?.experimentalStructuredNativeChat === true
const isMac = isMacUserAgent()
const isWindows = isWindowsUserAgent()
const isWebClient = isWebClientLocation()
@@ -161,6 +165,7 @@ export function useSettingsNavigationMetadata(): SettingsNavSection[] {
isLocalWindowsHost,
isWindowsTerminalHost,
isWebClient,
+ experimentalStructuredNativeChat,
managedBrowserCreationEnabled,
mobileEmulatorCreationEnabled,
isDev: import.meta.env.DEV,
@@ -178,6 +183,7 @@ export function useSettingsNavigationMetadata(): SettingsNavSection[] {
isLocalWindowsHost,
isWindowsTerminalHost,
isWebClient,
+ experimentalStructuredNativeChat,
managedBrowserCreationEnabled,
mobileEmulatorCreationEnabled,
isLinearConnected,
diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json
index 44df977da4a..f9071f05610 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -174,6 +174,9 @@
"retrying": "Retrying…"
},
"settings": {
+ "chat": {
+ "description": "Choose how chats look."
+ },
"terminal": {
"optionLayoutAlt": "ABC or U.S. — Option sends Alt/Esc sequences",
"optionLayoutCompose": "layout uses Option to compose characters like @, €, [, ]",
@@ -230,8 +233,7 @@
"wide": "Wide",
"full": "Full",
"pixels": "px",
- "reset": "Reset",
- "summary": "{{fontSize}}px · {{width}} width"
+ "reset": "Reset"
}
},
"browser": {
diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json
index 2668b63ebf9..0da94aca473 100644
--- a/src/renderer/src/i18n/locales/es.json
+++ b/src/renderer/src/i18n/locales/es.json
@@ -11,6 +11,9 @@
"retrying": "Reintentando…"
},
"settings": {
+ "chat": {
+ "description": "Elige el aspecto de los chats."
+ },
"terminal": {
"optionLayoutAlt": "ABC o EE. UU. — Option envía secuencias Alt/Esc",
"optionLayoutCompose": "la distribución usa Option para escribir caracteres como @, €, [, ]",
@@ -67,8 +70,7 @@
"wide": "Amplio",
"full": "Completo",
"pixels": "px",
- "reset": "Restablecer",
- "summary": "{{fontSize}}px · Ancho {{width}}"
+ "reset": "Restablecer"
}
},
"browser": {
diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json
index 868943016d4..1213c89d445 100644
--- a/src/renderer/src/i18n/locales/fr.json
+++ b/src/renderer/src/i18n/locales/fr.json
@@ -110,6 +110,9 @@
"retrying": "Nouvelle tentative…"
},
"settings": {
+ "chat": {
+ "description": "Choisissez l’apparence des chats."
+ },
"terminal": {
"optionLayoutAlt": "ABC ou U.S. — Option envoie des séquences Alt/Esc",
"optionLayoutCompose": "la disposition utilise Option pour composer des caractères comme @, €, [, ]",
@@ -166,8 +169,7 @@
"wide": "Large",
"full": "Pleine",
"pixels": "px",
- "reset": "Réinitialiser",
- "summary": "{{fontSize}}px · Largeur {{width}}"
+ "reset": "Réinitialiser"
}
},
"browser": {
diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json
index 2114a6ad839..066c752ceb8 100644
--- a/src/renderer/src/i18n/locales/ja.json
+++ b/src/renderer/src/i18n/locales/ja.json
@@ -11,6 +11,9 @@
"retrying": "再試行中…"
},
"settings": {
+ "chat": {
+ "description": "チャットの外観を選びます。"
+ },
"terminal": {
"optionLayoutAlt": "ABCまたはU.S. — OptionはAlt/Escシーケンスを送信します",
"optionLayoutCompose": "この配列ではOptionで@、€、[、]などの文字を入力します",
@@ -67,8 +70,7 @@
"wide": "広い",
"full": "全幅",
"pixels": "px",
- "reset": "リセット",
- "summary": "{{fontSize}}px · 幅: {{width}}"
+ "reset": "リセット"
}
},
"browser": {
diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json
index d46c5bbc508..a043ec36ea9 100644
--- a/src/renderer/src/i18n/locales/ko.json
+++ b/src/renderer/src/i18n/locales/ko.json
@@ -11,6 +11,9 @@
"retrying": "재시도 중…"
},
"settings": {
+ "chat": {
+ "description": "채팅 모양을 선택하세요."
+ },
"terminal": {
"optionLayoutAlt": "ABC 또는 U.S. — Option 키가 Alt/Esc 시퀀스를 전송합니다",
"optionLayoutCompose": "이 배열에서는 Option 키로 @, €, [, ] 같은 문자를 입력합니다",
@@ -67,8 +70,7 @@
"wide": "넓게",
"full": "전체",
"pixels": "px",
- "reset": "초기화",
- "summary": "{{fontSize}}px · {{width}} 너비"
+ "reset": "초기화"
}
},
"browser": {
diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json
index 3c032d9624a..3af1cb962ed 100644
--- a/src/renderer/src/i18n/locales/zh.json
+++ b/src/renderer/src/i18n/locales/zh.json
@@ -11,6 +11,9 @@
"retrying": "正在重试..."
},
"settings": {
+ "chat": {
+ "description": "选择聊天的外观。"
+ },
"terminal": {
"optionLayoutAlt": "ABC 或 U.S. — Option 键发送 Alt/Esc 序列",
"optionLayoutCompose": "此布局使用 Option 键输入 @、€、[、] 等字符",
@@ -67,8 +70,7 @@
"wide": "宽",
"full": "全宽",
"pixels": "px",
- "reset": "重置",
- "summary": "{{fontSize}}px · {{width}}宽度"
+ "reset": "重置"
}
},
"browser": {
diff --git a/src/renderer/src/lib/settings-navigation-types.ts b/src/renderer/src/lib/settings-navigation-types.ts
index 9aafdc3649c..358f59d29db 100644
--- a/src/renderer/src/lib/settings-navigation-types.ts
+++ b/src/renderer/src/lib/settings-navigation-types.ts
@@ -20,6 +20,7 @@ const SETTINGS_NAV_TARGETS = [
'git',
'tasks',
'appearance',
+ 'chat',
'input',
'floating-workspace',
'terminal',