@@ -4853,144 +4853,32 @@ describe('SettingsDialog notifications interactions', () => {
48534853 } ) ;
48544854} ) ;
48554855
4856- describe ( 'SettingsDialog appearance interactions' , ( ) => {
4856+ // Was 'SettingsDialog appearance interactions'. The eight theme/accent cases
4857+ // this block opened with are retired: the product removed theme selection
4858+ // outright ("主题设置不要了,因为 workspace 功能不支持暗色主题,要干掉"), which
4859+ // also formally overturns the NON-ALIGNMENT #9 note that had argued for keeping
4860+ // the segmented control as the last "follow system" entry point. The document
4861+ // theme/accent teardown went with them — nothing here writes those any more.
4862+ // What survives is the AMR draft-reconciliation coverage that merely happened
4863+ // to live in this block.
4864+ describe ( 'SettingsDialog draft reconciliation' , ( ) => {
48574865 afterEach ( ( ) => {
48584866 cleanup ( ) ;
4859- document . documentElement . removeAttribute ( 'data-theme' ) ;
4860- document . documentElement . style . removeProperty ( '--accent' ) ;
4861- document . documentElement . style . removeProperty ( '--accent-strong' ) ;
4862- document . documentElement . style . removeProperty ( '--accent-soft' ) ;
4863- document . documentElement . style . removeProperty ( '--accent-tint' ) ;
4864- document . documentElement . style . removeProperty ( '--accent-hover' ) ;
4865- } ) ;
4866-
4867- // #5517 drops the accent-colour swatches from Appearance but keeps the
4868- // 系统/浅色/深色 segmented control (its own comment calls the removal
4869- // *temporary*, and the account menu no longer carries a 切换主题 row, so this
4870- // is the product's only "follow system" entry point — NON-ALIGNMENT #9).
4871- it ( 'offers the theme segmented control, and System leaves the document theme unset' , ( ) => {
4872- const { container } = renderSettingsDialog (
4873- { theme : 'system' } ,
4874- { initialSection : 'appearance' } ,
4875- ) ;
4876-
4877- const group = screen . getByRole ( 'group' , { name : 'Appearance' } ) ;
4878- expect ( container . querySelector ( '.seg-control[aria-label="Appearance"]' ) ) . toBeTruthy ( ) ;
4879- expect ( within ( group ) . getByRole ( 'button' , { name : 'System' } ) . getAttribute ( 'aria-pressed' ) ) . toBe ( 'true' ) ;
4880- expect ( within ( group ) . getByRole ( 'button' , { name : 'Light' } ) . getAttribute ( 'aria-pressed' ) ) . toBe ( 'false' ) ;
4881- expect ( within ( group ) . getByRole ( 'button' , { name : 'Dark' } ) . getAttribute ( 'aria-pressed' ) ) . toBe ( 'false' ) ;
4882-
4883- // Regression guard: the accent swatches must not come back — #5517 removed
4884- // that control, and `accentColor` is now a stored value with no editor.
4885- expect ( screen . queryByRole ( 'radiogroup' , { name : 'Accent color' } ) ) . toBeNull ( ) ;
4886- expect ( screen . queryByLabelText ( 'Custom color' ) ) . toBeNull ( ) ;
4887-
4888- // `system` means "no explicit document theme", so the OS media query keeps
4889- // ownership.
4890- expect ( document . documentElement . hasAttribute ( 'data-theme' ) ) . toBe ( false ) ;
4891- } ) ;
4892-
4893- it ( 'applies the stored default accent color even though the picker is gone' , ( ) => {
4894- renderSettingsDialog (
4895- { theme : 'system' } ,
4896- { initialSection : 'appearance' } ,
4897- ) ;
4898-
4899- expect ( document . documentElement . style . getPropertyValue ( '--accent' ) ) . toBe ( '#353535' ) ;
4900- } ) ;
4901-
4902- it ( 'writes the picked theme to the document and autosaves it' , async ( ) => {
4903- const { onPersist } = renderSettingsDialog (
4904- { mode : 'daemon' , agentId : 'codex' , theme : 'system' } ,
4905- { initialSection : 'appearance' } ,
4906- ) ;
4907-
4908- const group = screen . getByRole ( 'group' , { name : 'Appearance' } ) ;
4909- fireEvent . click ( within ( group ) . getByRole ( 'button' , { name : 'Dark' } ) ) ;
4910- expect ( document . documentElement . getAttribute ( 'data-theme' ) ) . toBe ( 'dark' ) ;
4911-
4912- await waitForPersist ( onPersist , expect . objectContaining ( { theme : 'dark' } ) , { } ) ;
4913- } ) ;
4914-
4915- // The section still runs the live appearance preview on mount; it just reads
4916- // the theme instead of letting the user set it. Opening Settings after the
4917- // account menu flipped the theme must show that theme, and an explicit
4918- // theme/`system` must keep producing/removing `data-theme` respectively.
4919- // Each mount is one preview pass, so this drives the three modes by
4920- // remounting rather than by clicking the removed segmented control.
4921- it ( 'live previews the configured theme on open, and System leaves no explicit document theme' , ( ) => {
4922- renderSettingsDialog ( { theme : 'dark' } , { initialSection : 'appearance' } ) ;
4923- expect ( document . documentElement . getAttribute ( 'data-theme' ) ) . toBe ( 'dark' ) ;
4924- cleanup ( ) ;
4925-
4926- renderSettingsDialog ( { theme : 'light' } , { initialSection : 'appearance' } ) ;
4927- expect ( document . documentElement . getAttribute ( 'data-theme' ) ) . toBe ( 'light' ) ;
4928- cleanup ( ) ;
4929-
4930- renderSettingsDialog ( { theme : 'system' } , { initialSection : 'appearance' } ) ;
4931- expect ( document . documentElement . hasAttribute ( 'data-theme' ) ) . toBe ( false ) ;
4932- } ) ;
4933-
4934- // The close-time revert is unchanged: SettingsDialog's cleanup re-applies the
4935- // LAST SAVED appearance, so a preview the user never saved is rolled back.
4936- // The stored accent is asserted too, because cleanup re-applies theme AND
4937- // accent together and must not drop either on the way back.
4938- it ( 'reverts an unsaved appearance preview back to the saved appearance when the dialog closes' , ( ) => {
4939- const first = renderSettingsDialog (
4940- { theme : 'dark' , accentColor : '#2563eb' } ,
4941- { initialSection : 'appearance' } ,
4942- ) ;
4943-
4944- expect ( document . documentElement . getAttribute ( 'data-theme' ) ) . toBe ( 'dark' ) ;
4945- expect ( document . documentElement . style . getPropertyValue ( '--accent' ) ) . toBe ( '#2563eb' ) ;
4946-
4947- // Autosave is debounced (400ms), so closing immediately leaves this edit
4948- // unsaved — exactly the case the revert exists for.
4949- const group = screen . getByRole ( 'group' , { name : 'Appearance' } ) ;
4950- fireEvent . click ( within ( group ) . getByRole ( 'button' , { name : 'Light' } ) ) ;
4951- expect ( document . documentElement . getAttribute ( 'data-theme' ) ) . toBe ( 'light' ) ;
4952-
4953- fireEvent . click ( first . container . querySelector ( '.settings-close' ) as HTMLElement ) ;
4954- expect ( first . onClose ) . toHaveBeenCalledTimes ( 1 ) ;
4955-
4956- first . unmount ( ) ;
4957- expect ( document . documentElement . style . getPropertyValue ( '--accent' ) ) . toBe ( '#2563eb' ) ;
4958- expect ( document . documentElement . getAttribute ( 'data-theme' ) ) . toBe ( 'dark' ) ;
4959- } ) ;
4960-
4961- it ( 'persists System mode explicitly and preserves accent variables without an explicit document theme' , async ( ) => {
4962- const { onPersist } = renderSettingsDialog (
4963- { mode : 'daemon' , agentId : 'codex' , theme : 'light' , accentColor : '#2563eb' } ,
4964- { initialSection : 'appearance' } ,
4965- ) ;
4966-
4967- expect ( document . documentElement . getAttribute ( 'data-theme' ) ) . toBe ( 'light' ) ;
4968- expect ( document . documentElement . style . getPropertyValue ( '--accent' ) ) . toBe ( '#2563eb' ) ;
4969-
4970- // `system` must be persisted EXPLICITLY (not dropped to undefined), and it
4971- // must keep the stored accent variables applied while leaving the document
4972- // theme to the OS media query.
4973- const group = screen . getByRole ( 'group' , { name : 'Appearance' } ) ;
4974- fireEvent . click ( within ( group ) . getByRole ( 'button' , { name : 'System' } ) ) ;
4975- expect ( document . documentElement . hasAttribute ( 'data-theme' ) ) . toBe ( false ) ;
4976- expect ( document . documentElement . style . getPropertyValue ( '--accent' ) ) . toBe ( '#2563eb' ) ;
4977-
4978- await waitForPersist (
4979- onPersist ,
4980- expect . objectContaining ( {
4981- theme : 'system' ,
4982- accentColor : '#2563eb' ,
4983- } ) ,
4984- { } ,
4985- ) ;
49864867 } ) ;
49874868
49884869 it ( 'reconciles the open settings draft when the parent agent CLI env changes' , async ( ) => {
49894870 const view = renderSettingsDialog (
49904871 {
49914872 mode : 'daemon' ,
49924873 agentId : 'amr' ,
4993- theme : 'dark' ,
4874+ // Seeded on so the one click below is a real state change: the
4875+ // completion-sound pills are no-ops when clicked in their current state.
4876+ notifications : {
4877+ soundEnabled : true ,
4878+ successSoundId : 'chime' ,
4879+ failureSoundId : 'two-tone-down' ,
4880+ desktopEnabled : false ,
4881+ } ,
49944882 agentModels : {
49954883 amr : {
49964884 model : 'prod-only-model' ,
@@ -5005,7 +4893,7 @@ describe('SettingsDialog appearance interactions', () => {
50054893 } ,
50064894 } ,
50074895 } ,
5008- { initialSection : 'appearance ' , agents : [ amrAgent , ...availableAgents ] } ,
4896+ { initialSection : 'notifications ' , agents : [ amrAgent , ...availableAgents ] } ,
50094897 ) ;
50104898
50114899 view . rerender (
@@ -5014,7 +4902,12 @@ describe('SettingsDialog appearance interactions', () => {
50144902 ...baseConfig ,
50154903 mode : 'daemon' ,
50164904 agentId : 'amr' ,
5017- theme : 'dark' ,
4905+ notifications : {
4906+ soundEnabled : true ,
4907+ successSoundId : 'chime' ,
4908+ failureSoundId : 'two-tone-down' ,
4909+ desktopEnabled : false ,
4910+ } ,
50184911 agentCliEnv : {
50194912 amr : {
50204913 OPEN_DESIGN_AMR_PROFILE : 'local' ,
@@ -5025,7 +4918,7 @@ describe('SettingsDialog appearance interactions', () => {
50254918 agents = { [ amrAgent , ...availableAgents ] }
50264919 daemonLive = { true }
50274920 appVersionInfo = { null }
5028- initialSection = "appearance "
4921+ initialSection = "notifications "
50294922 onPersist = { view . onPersist }
50304923 onPersistComposioKey = { view . onPersistComposioKey }
50314924 onClose = { view . onClose }
@@ -5034,15 +4927,19 @@ describe('SettingsDialog appearance interactions', () => {
50344927 ) ;
50354928
50364929 // Any committed edit will do — this test is about what the draft carries
5037- // when it autosaves, not about which control fired it.
4930+ // when it autosaves, not about which control fired it. It used to ride the
4931+ // Appearance theme control; with theme selection removed, the notifications
4932+ // completion-sound toggle is the equivalent one-click persisted edit.
50384933 fireEvent . click (
5039- within ( screen . getByRole ( 'group' , { name : 'Appearance' } ) ) . getByRole ( 'button' , { name : 'Light' } ) ,
4934+ within ( screen . getByRole ( 'group' , { name : 'Completion sound' } ) ) . getByRole ( 'button' , {
4935+ name : 'inactive' ,
4936+ } ) ,
50404937 ) ;
50414938
50424939 await waitForPersist (
50434940 view . onPersist ,
50444941 expect . objectContaining ( {
5045- theme : 'light' ,
4942+ notifications : expect . objectContaining ( { soundEnabled : false } ) ,
50464943 agentModels : { } ,
50474944 agentCliEnv : {
50484945 codex : { CODEX_BIN : '/tmp/codex-dev' } ,
@@ -5117,64 +5014,6 @@ describe('SettingsDialog appearance interactions', () => {
51175014 } ,
51185015 } ) ;
51195016 } ) ;
5120-
5121- // #5517 removed the accent-colour editor from Settings, so a stored accent is
5122- // now read-only from this surface: it must still be applied on open and
5123- // survive a save driven by any other Appearance control, and it must not be
5124- // rewritten to the default just because nothing edits it any more.
5125- it ( 'keeps a stored non-default accent applied and carries it through an autosave' , async ( ) => {
5126- const view = renderSettingsDialog (
5127- { mode : 'daemon' , agentId : 'codex' , theme : 'light' , accentColor : '#2563eb' } ,
5128- { initialSection : 'appearance' } ,
5129- ) ;
5130-
5131- expect ( document . documentElement . style . getPropertyValue ( '--accent' ) ) . toBe ( '#2563eb' ) ;
5132-
5133- fireEvent . click (
5134- within ( screen . getByRole ( 'group' , { name : 'Appearance' } ) ) . getByRole ( 'button' , { name : 'Dark' } ) ,
5135- ) ;
5136-
5137- await waitForPersist (
5138- view . onPersist ,
5139- expect . objectContaining ( {
5140- theme : 'dark' ,
5141- accentColor : '#2563eb' ,
5142- } ) ,
5143- { } ,
5144- ) ;
5145-
5146- fireEvent . click ( view . container . querySelector ( '.settings-close' ) as HTMLElement ) ;
5147- expect ( view . onClose ) . toHaveBeenCalledTimes ( 1 ) ;
5148-
5149- view . unmount ( ) ;
5150- expect ( document . documentElement . style . getPropertyValue ( '--accent' ) ) . toBe ( '#2563eb' ) ;
5151- } ) ;
5152-
5153- it ( 'localizes the theme controls in Chinese' , ( ) => {
5154- render (
5155- < I18nProvider initial = "zh-CN" >
5156- < SettingsDialog
5157- initial = { { ...baseConfig , theme : 'light' } }
5158- agents = { availableAgents }
5159- daemonLive = { true }
5160- appVersionInfo = { null }
5161- initialSection = "appearance"
5162- onPersist = { vi . fn ( ) }
5163- onPersistComposioKey = { vi . fn ( ) }
5164- onClose = { vi . fn ( ) }
5165- onRefreshAgents = { vi . fn ( ) }
5166- />
5167- </ I18nProvider > ,
5168- ) ;
5169-
5170- const group = screen . getByRole ( 'group' , { name : '外观' } ) ;
5171- expect ( within ( group ) . getByRole ( 'button' , { name : '系统' } ) ) . toBeTruthy ( ) ;
5172- expect ( within ( group ) . getByRole ( 'button' , { name : '浅色' } ) ) . toBeTruthy ( ) ;
5173- expect ( within ( group ) . getByRole ( 'button' , { name : '深色' } ) ) . toBeTruthy ( ) ;
5174- // The accent picker and its Chinese labels are gone with it.
5175- expect ( screen . queryByRole ( 'radiogroup' , { name : '主题色' } ) ) . toBeNull ( ) ;
5176- expect ( screen . queryByLabelText ( '自定义颜色' ) ) . toBeNull ( ) ;
5177- } ) ;
51785017} ) ;
51795018
51805019describe ( 'SettingsDialog pets interactions' , ( ) => {
@@ -5623,17 +5462,26 @@ describe('SettingsDialog about interactions', () => {
56235462 mode : 'daemon' ,
56245463 agentId : 'codex' ,
56255464 onboardingCompleted : true ,
5626- theme : 'system' ,
5465+ // Seeded on so the one click below is a real state change.
5466+ notifications : {
5467+ soundEnabled : true ,
5468+ successSoundId : 'chime' ,
5469+ failureSoundId : 'two-tone-down' ,
5470+ desktopEnabled : false ,
5471+ } ,
56275472 } ,
56285473 {
5629- initialSection : 'appearance ' ,
5474+ initialSection : 'notifications ' ,
56305475 onResetOnboarding,
56315476 } ,
56325477 ) ;
56335478
5479+ // The subject is the pending-autosave drop, not which control queued it.
5480+ // Theme selection is gone, so the completion-sound toggle stands in as the
5481+ // one-click persisted edit that leaves a debounced save in flight.
56345482 fireEvent . click (
5635- within ( screen . getByRole ( 'group' , { name : 'Appearance ' } ) ) . getByRole ( 'button' , {
5636- name : 'Dark ' ,
5483+ within ( screen . getByRole ( 'group' , { name : 'Completion sound ' } ) ) . getByRole ( 'button' , {
5484+ name : 'inactive ' ,
56375485 } ) ,
56385486 ) ;
56395487 expect ( screen . getByText ( 'Saving…' ) ) . toBeTruthy ( ) ;
@@ -5646,7 +5494,7 @@ describe('SettingsDialog about interactions', () => {
56465494 expect ( onResetOnboarding ) . toHaveBeenCalledWith (
56475495 expect . objectContaining ( {
56485496 onboardingCompleted : false ,
5649- theme : 'dark' ,
5497+ notifications : expect . objectContaining ( { soundEnabled : false } ) ,
56505498 } ) ,
56515499 ) ;
56525500 } ) ;
@@ -6109,7 +5957,7 @@ describe('SettingsDialog about interactions', () => {
61095957 it ( 'still autosaves an unrelated edit that lands during a silent-update save' , async ( ) => {
61105958 // Regression: success must only advance autosaveLastSavedRef for
61115959 // allowSilentUpdates. Spreading the whole latest draft would mark a
6112- // concurrent theme (etc.) change as already saved and skip onPersist.
5960+ // concurrent unrelated change as already saved and skip onPersist.
61135961 let resolveSave : ( ( ) => void ) | null = null ;
61145962 const onSilentUpdatePreferenceChange = vi . fn (
61155963 ( ) => new Promise < void > ( ( resolve ) => {
@@ -6121,8 +5969,13 @@ describe('SettingsDialog about interactions', () => {
61215969 mode : 'daemon' ,
61225970 agentId : 'codex' ,
61235971 allowSilentUpdates : false ,
6124- theme : 'light' ,
6125- accentColor : '#2563eb' ,
5972+ // Seeded on so the concurrent click below is a real state change.
5973+ notifications : {
5974+ soundEnabled : true ,
5975+ successSoundId : 'chime' ,
5976+ failureSoundId : 'two-tone-down' ,
5977+ desktopEnabled : false ,
5978+ } ,
61265979 } ,
61275980 {
61285981 initialSection : 'about' ,
@@ -6147,18 +6000,18 @@ describe('SettingsDialog about interactions', () => {
61476000 expect ( onPersist ) . not . toHaveBeenCalled ( ) ;
61486001
61496002 // Concurrent persisted edit while the silent-update request is in flight.
6150- // #5517 dropped the accent-colour picker from Appearance, so the theme
6151- // segmented control is now the section's persisted-edit vehicle. The
6152- // invariant under test is the autosave bookkeeping, not the accent field .
6153- fireEvent . click ( screen . getByRole ( 'button' , { name : / A p p e a r a n c e / i } ) ) ;
6003+ // The invariant under test is the autosave bookkeeping, not the field that
6004+ // carries it — theme selection was the old vehicle and is gone, so this
6005+ // reaches for the notifications completion-sound toggle instead .
6006+ fireEvent . click ( screen . getByRole ( 'button' , { name : / G e n e r a l / i } ) ) ;
61546007 fireEvent . click (
6155- within ( screen . getByRole ( 'group' , { name : 'Appearance ' } ) ) . getByRole ( 'button' , {
6156- name : 'Dark ' ,
6008+ within ( screen . getByRole ( 'group' , { name : 'Completion sound ' } ) ) . getByRole ( 'button' , {
6009+ name : 'inactive ' ,
61576010 } ) ,
61586011 ) ;
61596012
61606013 // Resolve silent-update AFTER the concurrent edit is in draft. The success
6161- // path must not stamp this theme into autosaveLastSavedRef.
6014+ // path must not stamp this edit into autosaveLastSavedRef.
61626015 await act ( async ( ) => {
61636016 resolveSave ?.( ) ;
61646017 await Promise . resolve ( ) ;
@@ -6167,7 +6020,7 @@ describe('SettingsDialog about interactions', () => {
61676020 await waitForPersist (
61686021 onPersist ,
61696022 expect . objectContaining ( {
6170- theme : 'dark' ,
6023+ notifications : expect . objectContaining ( { soundEnabled : false } ) ,
61716024 } ) ,
61726025 { } ,
61736026 ) ;
0 commit comments