diff --git a/src/components/SettingsDialog.tsx b/src/components/SettingsDialog.tsx index 7b2f246f..f0150042 100644 --- a/src/components/SettingsDialog.tsx +++ b/src/components/SettingsDialog.tsx @@ -15,6 +15,7 @@ import { themeToCss, detectThemeTone } from '../lib/custom-theme'; import { store, setTerminalFont, + setTerminalScreenReaderMode, setAutoTrustFolders, setShowPlans, setShowPromptInput, @@ -463,6 +464,13 @@ export function SettingsDialog(props: SettingsDialogProps) { description="Enable antialiasing and geometric text rendering" align="flex-start" /> + diff --git a/src/components/TerminalView.tsx b/src/components/TerminalView.tsx index 01eda0a7..aef2bd1c 100644 --- a/src/components/TerminalView.tsx +++ b/src/components/TerminalView.tsx @@ -416,6 +416,7 @@ export function TerminalView(props: TerminalViewProps) { cursorBlink: true, fontSize: initialFontSize, fontFamily: getTerminalFontFamily(store.terminalFont), + screenReaderMode: store.terminalScreenReaderMode, theme: activeTerminalTheme(), allowProposedApi: true, scrollback: TERMINAL_SCROLLBACK_LINES, @@ -1107,6 +1108,12 @@ export function TerminalView(props: TerminalViewProps) { markDirty(props.agentId); }); + createEffect(() => { + const screenReaderMode = store.terminalScreenReaderMode; + if (!term) return; + term.options.screenReaderMode = screenReaderMode; + }); + createEffect(() => { if (!term) return; term.options.theme = activeTerminalTheme(); diff --git a/src/store/autosave.test.ts b/src/store/autosave.test.ts index c9543e09..4cbab112 100644 --- a/src/store/autosave.test.ts +++ b/src/store/autosave.test.ts @@ -30,6 +30,15 @@ describe('autosave snapshot includes new-task-default fields', () => { setStore('defaultPropagateSkipPermissions', false); }); + it('terminalScreenReaderMode changes the snapshot', () => { + setStore('terminalScreenReaderMode', false); + const before = persistedSnapshot(); + setStore('terminalScreenReaderMode', true); + const after = persistedSnapshot(); + expect(before).not.toBe(after); + setStore('terminalScreenReaderMode', false); + }); + it('showSteps is not tracked separately (migrated to defaultStepsEnabled)', () => { expect('showSteps' in store).toBe(false); }); diff --git a/src/store/autosave.ts b/src/store/autosave.ts index 9b269c89..7eef7b33 100644 --- a/src/store/autosave.ts +++ b/src/store/autosave.ts @@ -21,6 +21,7 @@ export function persistedSnapshot(): string { mergedLinesAdded: store.mergedLinesAdded, mergedLinesRemoved: store.mergedLinesRemoved, terminalFont: store.terminalFont, + terminalScreenReaderMode: store.terminalScreenReaderMode, themePreset: store.themePreset, windowState: store.windowState, autoTrustFolders: store.autoTrustFolders, diff --git a/src/store/core.ts b/src/store/core.ts index 016747cd..8d9addf1 100644 --- a/src/store/core.ts +++ b/src/store/core.ts @@ -37,6 +37,7 @@ export const [store, setStore] = createStore({ mergedLinesAdded: 0, mergedLinesRemoved: 0, terminalFont: DEFAULT_TERMINAL_FONT, + terminalScreenReaderMode: false, themePreset: 'islands-dark', appearanceMode: 'dark', lightThemePreset: 'islands-light', diff --git a/src/store/persistence.test.ts b/src/store/persistence.test.ts index 6ae20978..71043ee7 100644 --- a/src/store/persistence.test.ts +++ b/src/store/persistence.test.ts @@ -80,6 +80,7 @@ beforeEach(() => { setStore('customAgents', []); setStore('coordinatorControlHintDismissed', false); setStore('autoStartRemoteAccess', false); + setStore('terminalScreenReaderMode', false); }); describe('resolveIncomingPanelUserSize', () => { @@ -490,6 +491,53 @@ describe('loadState theme persistence', () => { }); }); +describe('terminal screen reader mode persistence', () => { + it('defaults to false when absent from saved state', async () => { + mockInvoke.mockResolvedValueOnce(basePayload()); + + await loadState(); + + expect(store.terminalScreenReaderMode).toBe(false); + }); + + it('restores an enabled screen reader mode', async () => { + mockInvoke.mockResolvedValueOnce(basePayload({ terminalScreenReaderMode: true })); + + await loadState(); + + expect(store.terminalScreenReaderMode).toBe(true); + }); + + it.each(['true', 1, null])('rejects a non-boolean persisted value (%s)', async (value) => { + setStore('terminalScreenReaderMode', true); + mockInvoke.mockResolvedValueOnce(basePayload({ terminalScreenReaderMode: value })); + + await loadState(); + + expect(store.terminalScreenReaderMode).toBe(false); + }); + + it('omits the disabled default when saving', async () => { + setStore('terminalScreenReaderMode', false); + mockInvoke.mockResolvedValueOnce(undefined); + + await saveState(); + + const saved = JSON.parse(mockInvoke.mock.calls[0][1].json); + expect(saved.terminalScreenReaderMode).toBeUndefined(); + }); + + it('persists the enabled setting', async () => { + setStore('terminalScreenReaderMode', true); + mockInvoke.mockResolvedValueOnce(undefined); + + await saveState(); + + const saved = JSON.parse(mockInvoke.mock.calls[0][1].json); + expect(saved.terminalScreenReaderMode).toBe(true); + }); +}); + describe('coordinator control hint persistence', () => { it('does not persist dismissed=false', async () => { setStore('coordinatorControlHintDismissed', false); diff --git a/src/store/persistence.ts b/src/store/persistence.ts index 33086734..f0619bff 100644 --- a/src/store/persistence.ts +++ b/src/store/persistence.ts @@ -183,6 +183,7 @@ export async function saveState(): Promise { mergedLinesAdded: store.mergedLinesAdded, mergedLinesRemoved: store.mergedLinesRemoved, terminalFont: store.terminalFont, + terminalScreenReaderMode: store.terminalScreenReaderMode || undefined, themePreset: store.themePreset, showPromptInput: store.showPromptInput, fontSmoothing: store.fontSmoothing, @@ -351,6 +352,7 @@ interface LegacyPersistedState { mergedLinesAdded?: unknown; mergedLinesRemoved?: unknown; terminalFont?: unknown; + terminalScreenReaderMode?: unknown; themePreset?: unknown; showPromptInput?: unknown; fontSmoothing?: unknown; @@ -497,6 +499,8 @@ export async function loadState(): Promise { typeof raw.terminalFont === 'string' && raw.terminalFont.trim() ? raw.terminalFont : DEFAULT_TERMINAL_FONT; + s.terminalScreenReaderMode = + typeof raw.terminalScreenReaderMode === 'boolean' ? raw.terminalScreenReaderMode : false; s.themePreset = isLookPreset(raw.themePreset) ? raw.themePreset : 'minimal'; s.showPromptInput = typeof raw.showPromptInput === 'boolean' ? raw.showPromptInput : true; s.fontSmoothing = typeof raw.fontSmoothing === 'boolean' ? raw.fontSmoothing : true; diff --git a/src/store/store.ts b/src/store/store.ts index 8e8c35a9..e242b2c6 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -117,6 +117,7 @@ export { toggleTaskFocusMode, setTaskSplitMode, setTerminalFont, + setTerminalScreenReaderMode, applyAppearanceMode, markCustomThemesReady, setAppearanceMode, diff --git a/src/store/types.ts b/src/store/types.ts index e1040baf..1feb8573 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -249,6 +249,7 @@ export interface PersistedState { mergedLinesAdded?: number; mergedLinesRemoved?: number; terminalFont?: string; + terminalScreenReaderMode?: boolean; themePreset?: LookPreset; showPromptInput?: boolean; fontSmoothing?: boolean; @@ -346,6 +347,7 @@ export interface AppStore { mergedLinesAdded: number; mergedLinesRemoved: number; terminalFont: string; + terminalScreenReaderMode: boolean; themePreset: LookPreset; showPromptInput: boolean; fontSmoothing: boolean; diff --git a/src/store/ui.test.ts b/src/store/ui.test.ts index 75591752..c9939fb4 100644 --- a/src/store/ui.test.ts +++ b/src/store/ui.test.ts @@ -9,6 +9,7 @@ type MockStore = { panelUserSize: Record; projectsCollapsed: boolean; sidebarFocusedProjectId: string | null; + terminalScreenReaderMode: boolean; }; let mockStore: MockStore; @@ -63,6 +64,7 @@ import { setPanelUserSize, deletePanelUserSize, setProjectsCollapsed, + setTerminalScreenReaderMode, } from './ui'; beforeEach(() => { @@ -78,6 +80,7 @@ beforeEach(() => { panelUserSize: {}, projectsCollapsed: false, sidebarFocusedProjectId: null, + terminalScreenReaderMode: false, }); vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => { @@ -195,3 +198,11 @@ describe('setProjectsCollapsed', () => { expect(mockStore.sidebarFocusedProjectId).toBeNull(); }); }); + +describe('setTerminalScreenReaderMode', () => { + it('updates the global terminal accessibility option', () => { + setTerminalScreenReaderMode(true); + + expect(mockStore.terminalScreenReaderMode).toBe(true); + }); +}); diff --git a/src/store/ui.ts b/src/store/ui.ts index 9368b214..e528818b 100644 --- a/src/store/ui.ts +++ b/src/store/ui.ts @@ -86,6 +86,10 @@ export function setTerminalFont(terminalFont: string): void { setStore('terminalFont', terminalFont); } +export function setTerminalScreenReaderMode(enabled: boolean): void { + setStore('terminalScreenReaderMode', enabled); +} + export function applyAppearanceMode(): void { const isDark = osIsDark(); const mode = store.appearanceMode;