mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(settings): always show Developer & Diagnostics (drop dev-mode gate) (#3639)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user