mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 08:02:21 +00:00
Add a Chat settings page for structured native chat (#25685)
* Add a Chat settings page for structured native chat * Remove unused chat appearance summary translations * Fix chat settings search entries and preview anchor * Keep shortcut formatting available in settings sidebar tests * Clarify the Chat settings page description * Update Chat settings test and remove unused summary alias
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -38,7 +38,11 @@ export function AppearanceChatSection({
|
||||
}
|
||||
return (
|
||||
<div className="divide-y divide-border/40">
|
||||
<SearchableSetting {...entries.textSize} forceVisible={forceVisiblePrimary}>
|
||||
<SearchableSetting
|
||||
id={entries.textSize.targetSectionId}
|
||||
{...entries.textSize}
|
||||
forceVisible={forceVisiblePrimary}
|
||||
>
|
||||
<NumberField
|
||||
label={entries.textSize.title}
|
||||
description={entries.textSize.description}
|
||||
@@ -51,7 +55,11 @@ export function AppearanceChatSection({
|
||||
onChange={(fontSize) => update({ fontSize })}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
<SearchableSetting {...entries.codeTextSize} forceVisible={forceVisiblePrimary}>
|
||||
<SearchableSetting
|
||||
id={entries.codeTextSize.targetSectionId}
|
||||
{...entries.codeTextSize}
|
||||
forceVisible={forceVisiblePrimary}
|
||||
>
|
||||
<NumberField
|
||||
label={entries.codeTextSize.title}
|
||||
description={entries.codeTextSize.description}
|
||||
@@ -64,7 +72,11 @@ export function AppearanceChatSection({
|
||||
onChange={(codeFontSize) => update({ codeFontSize })}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
<SearchableSetting {...entries.width} forceVisible={forceVisiblePrimary}>
|
||||
<SearchableSetting
|
||||
id={entries.width.targetSectionId}
|
||||
{...entries.width}
|
||||
forceVisible={forceVisiblePrimary}
|
||||
>
|
||||
<SettingsRow
|
||||
label={entries.width.title}
|
||||
description={entries.width.description}
|
||||
@@ -78,7 +90,11 @@ export function AppearanceChatSection({
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
<SearchableSetting {...entries.reset} forceVisible={forceVisiblePrimary}>
|
||||
<SearchableSetting
|
||||
id={entries.reset.targetSectionId}
|
||||
{...entries.reset}
|
||||
forceVisible={forceVisiblePrimary}
|
||||
>
|
||||
<SettingsRow
|
||||
label={entries.reset.title}
|
||||
description={entries.reset.description}
|
||||
|
||||
@@ -244,6 +244,16 @@ describe('AppearancePane', () => {
|
||||
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<HTMLButtonElement>('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<HTMLButtonElement>('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'
|
||||
|
||||
@@ -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({
|
||||
</AppearanceSection>
|
||||
) : null}
|
||||
|
||||
{chatMatches ? (
|
||||
<AppearanceSection
|
||||
id="chat"
|
||||
icon={<MessageSquare aria-hidden="true" />}
|
||||
title={chatTitle}
|
||||
summary={chatSummary}
|
||||
open={isSectionOpen('chat')}
|
||||
onToggle={() => toggleSection('chat')}
|
||||
toggleDisabled={isSearching}
|
||||
>
|
||||
<AppearanceChatSection
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
forceVisiblePrimary={chatLabelMatches}
|
||||
/>
|
||||
</AppearanceSection>
|
||||
) : null}
|
||||
|
||||
{windowMatches ? (
|
||||
<AppearanceSection
|
||||
id="window"
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { useRef, useState } from 'react'
|
||||
import { cleanup, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getDefaultSettings } from '../../../../shared/constants'
|
||||
import { buildSettingsNavigationMetadata } from '@/hooks/useSettingsNavigationMetadata'
|
||||
import { ChatSettingsSection } from './ChatSettingsSection'
|
||||
import { ActiveSettingsSectionProvider } from './SettingsSection'
|
||||
import { getChatAppearanceSearchEntries } from './chat-appearance-search'
|
||||
import { useSettingsRepoScrollEffects } from './use-settings-repo-scroll-effects'
|
||||
import type { SettingsStoreModel } from './use-settings-store-model'
|
||||
import type { SettingsInteractionController } from './use-settings-interaction-controller'
|
||||
import type { SettingsNavigationModel } from './use-settings-navigation-model'
|
||||
import type { SettingsTerminalModel } from './use-settings-terminal-model'
|
||||
|
||||
vi.mock('../../store', () => ({
|
||||
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<string | null>('chat')
|
||||
const pendingScrollTargetRef = useRef<string | null>('chat-code-text-size')
|
||||
const contentScrollRef = useRef<HTMLDivElement>(null)
|
||||
const pendingScrollTargetWatchRef = useRef(null)
|
||||
const pendingSubsectionScrollFrameRef = useRef<number | null>(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<SettingsTerminalModel, 'neededRepos'> = { 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 (
|
||||
<ActiveSettingsSectionProvider value={activeSectionId}>
|
||||
<output aria-label="Selected settings page">{activeSectionId}</output>
|
||||
<ChatSettingsSection
|
||||
settings={settings}
|
||||
updateSettings={vi.fn()}
|
||||
searchEntries={getChatAppearanceSearchEntries()}
|
||||
isMounted
|
||||
/>
|
||||
</ActiveSettingsSectionProvider>
|
||||
)
|
||||
}
|
||||
|
||||
describe('Chat settings deep links', () => {
|
||||
it('activates Chat and renders the moved row for a deep link', async () => {
|
||||
render(<NavigationHarness enabled initialSection="appearance" />)
|
||||
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(<NavigationHarness enabled={false} initialSection="chat" />)
|
||||
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(<NavigationHarness enabled={false} initialSection="appearance" />)
|
||||
expect(screen.getByRole('status', { name: 'Selected settings page' }).textContent).toBe(
|
||||
'appearance'
|
||||
)
|
||||
expect(container.querySelector('#chat')).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -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<GlobalSettings>) => {
|
||||
if (state.settings) {
|
||||
state.settings = { ...state.settings, ...updates }
|
||||
}
|
||||
})
|
||||
const element = (active = 'chat') => (
|
||||
<ActiveSettingsSectionProvider value={active}>
|
||||
<ChatSettingsSection
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
searchEntries={getChatAppearanceSearchEntries()}
|
||||
isMounted
|
||||
/>
|
||||
</ActiveSettingsSectionProvider>
|
||||
)
|
||||
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(
|
||||
<ActiveSettingsSectionProvider value="chat">
|
||||
<ChatSettingsSection
|
||||
settings={{ ...getDefaultSettings('/tmp'), experimentalStructuredNativeChat: false }}
|
||||
updateSettings={vi.fn()}
|
||||
searchEntries={[]}
|
||||
isMounted
|
||||
/>
|
||||
</ActiveSettingsSectionProvider>
|
||||
)
|
||||
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()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -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<GlobalSettings>) => 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 (
|
||||
<SettingsSection
|
||||
id="chat"
|
||||
title={title}
|
||||
description={translate('settings.chat.description', 'Choose how chats look.')}
|
||||
searchEntries={searchEntries}
|
||||
>
|
||||
{isMounted ? (
|
||||
<div id="chat-appearance" className="space-y-3">
|
||||
<SettingsSubsectionHeader title={appearanceTitle} />
|
||||
<AppearanceChatSection
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
forceVisiblePrimary={matchesSettingsSearch(query, [
|
||||
{ title },
|
||||
{ title: appearanceTitle }
|
||||
])}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
)
|
||||
}
|
||||
@@ -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<typeof ShortcutLabels>('@/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(
|
||||
<TooltipProvider>
|
||||
<SettingsSidebar
|
||||
activeSectionId="appearance"
|
||||
settings={settings}
|
||||
generalGroups={[{ id: 'interface', title: 'Interface', sections }]}
|
||||
repoSections={[]}
|
||||
hasRepos={false}
|
||||
onBack={vi.fn()}
|
||||
onSelectSection={vi.fn()}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
)
|
||||
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'),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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() {
|
||||
|
||||
+13
@@ -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 (
|
||||
<ChatSettingsSection
|
||||
settings={model.settings}
|
||||
updateSettings={model.updateSettings}
|
||||
searchEntries={navigation.getSectionSearchEntries('chat')}
|
||||
isMounted={view.isSectionMounted('chat')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function renderInputSettingsSection(context: SettingsRenderContext): React.JSX.Element {
|
||||
const { model, navigation } = context
|
||||
return (
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -7,6 +7,7 @@ export type SettingsNavigationBuildOptions = {
|
||||
isLocalWindowsHost: boolean
|
||||
isWindowsTerminalHost: boolean
|
||||
isWebClient: boolean
|
||||
experimentalStructuredNativeChat: boolean
|
||||
managedBrowserCreationEnabled: boolean
|
||||
mobileEmulatorCreationEnabled: boolean
|
||||
isDev: boolean
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -20,6 +20,7 @@ const SETTINGS_NAV_TARGETS = [
|
||||
'git',
|
||||
'tasks',
|
||||
'appearance',
|
||||
'chat',
|
||||
'input',
|
||||
'floating-workspace',
|
||||
'terminal',
|
||||
|
||||
Reference in New Issue
Block a user