feat(settings): always show Developer & Diagnostics (drop dev-mode gate) (#3639)

This commit is contained in:
Steven Enamakel
2026-06-12 13:32:39 -07:00
committed by GitHub
parent 9af599466f
commit 3a71619a48
5 changed files with 28 additions and 135 deletions
+15 -21
View File
@@ -1,6 +1,5 @@
import { type ReactNode, useState } from 'react';
import { useDeveloperMode } from '../../hooks/useDeveloperMode';
import { useT } from '../../lib/i18n/I18nContext';
import LanguageSelect from '../LanguageSelect';
import SettingsHeader from './components/SettingsHeader';
@@ -229,7 +228,6 @@ const GroupHeader = ({ label }: { label: string }) =>
const SettingsHome = () => {
const { navigateToSettings } = useSettingsNavigation();
const { t } = useT();
const developerMode = useDeveloperMode();
// Global settings search. While a query is active the normal menu is hidden
// and the search bar renders its own ranked result list instead.
@@ -375,26 +373,22 @@ const SettingsHome = () => {
],
};
// --- Developer & Diagnostics (gated) ---
// Hidden when developer mode is off.
// About is always accessible — that's where the toggle lives.
const developerGroup: SettingsGroup | null = developerMode
? {
id: 'developer',
label: '',
items: [
{
id: 'developer-options',
title: t('settings.developerDiagnostics'),
description: t('settings.developerDiagnosticsDesc'),
icon: DeveloperIcon,
onClick: () => navigateToSettings('developer-options'),
},
],
}
: null;
// --- Developer & Diagnostics (always visible) ---
const developerGroup: SettingsGroup = {
id: 'developer',
label: '',
items: [
{
id: 'developer-options',
title: t('settings.developerDiagnostics'),
description: t('settings.developerDiagnosticsDesc'),
icon: DeveloperIcon,
onClick: () => navigateToSettings('developer-options'),
},
],
};
const trailingGroups: SettingsGroup[] = [...(developerGroup ? [developerGroup] : []), aboutGroup];
const trailingGroups: SettingsGroup[] = [developerGroup, aboutGroup];
return (
<div className="z-10 relative">
@@ -264,25 +264,18 @@ describe('SettingsHome', () => {
expect(screen.queryByText('Billing & Usage')).not.toBeInTheDocument();
});
it('navigates to developer-options when "Developer & Diagnostics" is clicked (developerMode=true)', async () => {
it('navigates to developer-options when "Developer & Diagnostics" is clicked', async () => {
const user = userEvent.setup();
renderSettingsHome({ developerMode: true });
renderSettingsHome();
await user.click(screen.getByTestId('settings-nav-developer-options'));
expect(mockNavigateToSettings).toHaveBeenCalledWith('developer-options');
});
});
describe('developer mode gate', () => {
it('hides the developer-options entry when developerMode is off', () => {
describe('developer & diagnostics (always visible)', () => {
it('shows the developer-options entry regardless of the developerMode preference', () => {
renderSettingsHome({ developerMode: false });
expect(screen.queryByTestId('settings-nav-developer-options')).not.toBeInTheDocument();
// The English resolved text should also be absent
expect(screen.queryByText('Developer & Diagnostics')).not.toBeInTheDocument();
});
it('shows the developer-options entry when developerMode is on', () => {
renderSettingsHome({ developerMode: true });
expect(screen.getByTestId('settings-nav-developer-options')).toBeInTheDocument();
// useT() resolves to English even without I18nProvider
expect(screen.getByText('Developer & Diagnostics')).toBeInTheDocument();
@@ -7,27 +7,21 @@
* here would race with that component's own state machine.
*/
import { invoke } from '@tauri-apps/api/core';
import debug from 'debug';
import { useEffect, useState } from 'react';
import { useAppUpdate } from '../../../hooks/useAppUpdate';
import { useDeveloperMode } from '../../../hooks/useDeveloperMode';
import { useT } from '../../../lib/i18n/I18nContext';
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
import { selectDeveloperMode, setDeveloperMode } from '../../../store/themeSlice';
import { useAppSelector } from '../../../store/hooks';
import { APP_VERSION, LATEST_APP_DOWNLOAD_URL } from '../../../utils/config';
import { isTauriEnvironment } from '../../../utils/configPersistence';
import { openUrl } from '../../../utils/openUrl';
import Button from '../../ui/Button';
import SettingsHeader from '../components/SettingsHeader';
import { SettingsRow, SettingsSection, SettingsSwitch } from '../controls';
import { SettingsRow, SettingsSection } from '../controls';
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
const log = debug('settings:developer-mode');
const AboutPanel = () => {
const { t } = useT();
const dispatch = useAppDispatch();
const { navigateBack, breadcrumbs } = useSettingsNavigation();
// The auto-cadence is already running via the global <AppUpdatePrompt />;
// disable it here so opening the panel doesn't double-trigger probes.
@@ -35,12 +29,6 @@ const AboutPanel = () => {
const [lastCheckedAt, setLastCheckedAt] = useState<Date | null>(null);
const coreMode = useAppSelector(state => state.coreMode.mode);
const [rpcUrl, setRpcUrl] = useState<string | null>(null);
// Persisted developer mode preference (not the combined IS_DEV || developerMode).
// We read the raw preference here so the toggle reflects only the user's choice,
// not whether the build is a dev build.
const developerModePref = useAppSelector(selectDeveloperMode);
// Combined gate — true when IS_DEV or the pref is on. Used for the helper text.
const developerModeActive = useDeveloperMode();
// Local mode picks a dynamic port at app launch, so the authoritative
// value lives in the Tauri shell (`core_rpc_url` command) rather than the
@@ -161,33 +149,6 @@ const AboutPanel = () => {
</div>
</SettingsSection>
{/* Developer Mode toggle — always visible so users can enable it
without needing it to be on first (chicken-and-egg avoidance). */}
<div data-testid="developer-mode-section">
<SettingsSection>
<SettingsRow
htmlFor="switch-developer-mode"
label={t('settings.developerMode.title')}
description={
developerModeActive && !developerModePref
? t('settings.developerMode.enabledByBuild')
: t('settings.developerMode.description')
}
control={
<SettingsSwitch
id="switch-developer-mode"
checked={developerModePref}
onCheckedChange={next => {
log('toggled to %s', String(next));
dispatch(setDeveloperMode(next));
}}
aria-label={t('settings.developerMode.title')}
/>
}
/>
</SettingsSection>
</div>
{/* Releases */}
<SettingsSection>
<div className="px-4 py-4 space-y-2">
@@ -151,54 +151,3 @@ describe('AboutPanel', () => {
});
});
});
describe('AboutPanel — Developer Mode toggle', () => {
beforeEach(() => {
statusListeners.length = 0;
mockCheckAppUpdate.mockReset();
hoisted.mockIsTauri.mockReturnValue(false); // Simplify — no Tauri IPC for these tests
});
// useT() falls back to English even without I18nProvider (resolveEn() lookup).
// The toggle's aria-label is set to t('settings.developerMode.title') which
// resolves to the English string "Developer mode".
const TOGGLE_ARIA_LABEL = /Developer mode/i;
it('renders the Developer mode switch section', () => {
renderWithProviders(<AboutPanel />);
expect(screen.getByRole('switch', { name: TOGGLE_ARIA_LABEL })).toBeInTheDocument();
expect(screen.getByTestId('developer-mode-section')).toBeInTheDocument();
});
it('switch defaults to off (aria-checked=false)', () => {
renderWithProviders(<AboutPanel />);
const toggle = screen.getByRole('switch', { name: TOGGLE_ARIA_LABEL });
expect(toggle).toHaveAttribute('aria-checked', 'false');
});
it('turns on developer mode when toggle is clicked', () => {
const { store } = renderWithProviders(<AboutPanel />);
const toggle = screen.getByRole('switch', { name: TOGGLE_ARIA_LABEL });
fireEvent.click(toggle);
expect(store.getState().theme.developerMode).toBe(true);
expect(toggle).toHaveAttribute('aria-checked', 'true');
});
it('toggles developer mode off when clicked again', () => {
const { store } = renderWithProviders(<AboutPanel />, {
preloadedState: {
theme: {
mode: 'system',
tabBarLabels: 'hover',
fontSize: 'medium',
agentMessageViewMode: 'bubbles',
developerMode: true,
},
},
});
const toggle = screen.getByRole('switch', { name: TOGGLE_ARIA_LABEL });
expect(toggle).toHaveAttribute('aria-checked', 'true');
fireEvent.click(toggle);
expect(store.getState().theme.developerMode).toBe(false);
});
});
+7 -11
View File
@@ -1,18 +1,14 @@
/**
* useDeveloperMode — runtime developer-mode gate.
*
* Returns `true` when developer surfaces should be visible. The gate is open
* when EITHER the build is a Vite dev build (`IS_DEV`) OR the user has
* enabled Developer Mode in Settings About.
* Developer surfaces (Settings Developer & Diagnostics, dev-only settings
* search entries, the Intelligence "council" tab) are now always visible, so
* this hook unconditionally returns `true`. The previous opt-in toggle in
* Settings About has been removed.
*
* Gating is UI-only. The Rust `SecurityPolicy` / autonomy-tier enforcement
* in the core is authoritative and is never relaxed by this toggle.
* Gating was always UI-only. The Rust `SecurityPolicy` / autonomy-tier
* enforcement in the core is authoritative and is unaffected by this.
*/
import { useAppSelector } from '../store/hooks';
import { selectDeveloperMode } from '../store/themeSlice';
import { IS_DEV } from '../utils/config';
export function useDeveloperMode(): boolean {
const persistedMode = useAppSelector(selectDeveloperMode);
return IS_DEV || persistedMode;
return true;
}