Files
openchamber/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx
T
Bohdan Triapitsyn 2ec0238e5c feat: add an always-show scrollbars preference
Adds a device-local setting to keep overlay scrollbars visible.
Surfaces the setting in visual settings and settings search.
Updates scrollbar behavior and tests for the new preference.
2026-09-07 23:16:36 +03:00

256 lines
9.3 KiB
TypeScript

import React from 'react';
import { OpenChamberVisualSettings } from './OpenChamberVisualSettings';
import { AboutSettings } from './AboutSettings';
import { SessionRetentionSettings } from './SessionRetentionSettings';
import { PasskeySettings } from './PasskeySettings';
import { AppLinkSecuritySettings } from './AppLinkSecuritySettings';
import { DefaultsSettings } from './DefaultsSettings';
import { GitSettings } from './GitSettings';
import { NotificationSettings } from './NotificationSettings';
import { VoiceSettings } from './VoiceSettings';
import { TunnelSettings } from './TunnelSettings';
import { OpenCodeCliSettings } from './OpenCodeCliSettings';
import { OpenChamberToolsSettings } from './OpenChamberToolsSettings';
import { DesktopNetworkSettings } from './DesktopNetworkSettings';
import { KeyboardShortcutsSettings } from './KeyboardShortcutsSettings';
import { SettingsPageLayout } from '@/components/sections/shared/SettingsPageLayout';
import { useDeviceInfo } from '@/lib/device';
import { isDesktopLocalOriginActive, isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
import { isCapacitorApp } from '@/lib/platform';
import { useI18n } from '@/lib/i18n';
import { subscribeRuntimeEndpointChanged } from '@/lib/runtime-switch';
import type { OpenChamberSection } from './types';
const useRuntimeEndpointEpoch = (): number => {
const [epoch, setEpoch] = React.useState(0);
React.useEffect(() => {
return subscribeRuntimeEndpointChanged(() => setEpoch((current) => current + 1));
}, []);
return epoch;
};
interface OpenChamberPageProps {
/** Which section to display. If undefined, shows all sections (mobile/legacy behavior) */
section?: OpenChamberSection;
}
export const OpenChamberPage: React.FC<OpenChamberPageProps> = ({ section }) => {
const { t } = useI18n();
const { isMobile } = useDeviceInfo();
const runtimeEndpointEpoch = useRuntimeEndpointEpoch();
const showAbout = isMobile && isWebRuntime();
const isVSCode = isVSCodeRuntime();
void runtimeEndpointEpoch;
const showDesktopNetworkSettings = isDesktopShell() && isDesktopLocalOriginActive();
// If no section specified, show all (mobile/legacy behavior)
if (!section) {
return (
<SettingsPageLayout showSaveStatus className="openchamber-page-body space-y-3 sm:space-y-6">
<OpenChamberVisualSettings />
<DefaultsSettings />
{showDesktopNetworkSettings && <DesktopNetworkSettings />}
{!isVSCode && <OpenCodeCliSettings />}
{!isVSCode && <OpenChamberToolsSettings />}
<SessionRetentionSettings />
<AppLinkSecuritySettings />
{isWebRuntime() && !isDesktopShell() && !isVSCode && !isCapacitorApp() && <PasskeySettings />}
{showAbout && <AboutSettings />}
</SettingsPageLayout>
);
}
// Show specific section content
const renderSectionContent = () => {
switch (section) {
case 'general':
return <GeneralSectionContent />;
case 'visual':
return <VisualSectionContent />;
case 'chat':
return <ChatSectionContent />;
case 'sessions':
return <SessionsSectionContent />;
case 'shortcuts':
return <ShortcutsSectionContent />;
case 'git':
return <GitSectionContent />;
case 'notifications':
return <NotificationSectionContent />;
case 'voice':
return <VoiceSectionContent />;
case 'tunnel':
return <TunnelSectionContent />;
default:
return null;
}
};
const pageTitle = {
general: t('settings.page.general.title'),
visual: t('settings.page.appearance.title'),
chat: t('settings.page.chat.title'),
sessions: t('settings.page.sessions.title'),
shortcuts: t('settings.page.shortcuts.title'),
git: t('settings.page.git.title'),
github: t('settings.page.git.title'),
notifications: t('settings.page.notifications.title'),
voice: t('settings.page.voice.title'),
tunnel: t('settings.page.tunnel.title'),
}[section];
const pageDescription = {
general: t('settings.page.general.description'),
visual: t('settings.page.appearance.description'),
chat: t('settings.page.chat.description'),
sessions: t('settings.page.sessions.description'),
shortcuts: t('settings.page.shortcuts.description'),
git: undefined,
github: undefined,
notifications: t('settings.page.notifications.description'),
voice: t('settings.page.voice.description'),
tunnel: t('settings.page.tunnel.description'),
}[section];
return (
<SettingsPageLayout
title={pageTitle}
description={pageDescription}
showSaveStatus
className="openchamber-page-body"
>
{renderSectionContent()}
</SettingsPageLayout>
);
};
const ShortcutsSectionContent: React.FC = () => {
return <KeyboardShortcutsSettings />;
};
// General section: app-level settings — startup/tray/network, access password,
// passkeys, OpenCode CLI binary, message stream transport, privacy.
const GeneralSectionContent: React.FC = () => {
const isVSCode = isVSCodeRuntime();
const runtimeEndpointEpoch = useRuntimeEndpointEpoch();
void runtimeEndpointEpoch;
const showDesktopNetworkSettings = isDesktopShell() && isDesktopLocalOriginActive();
// Passkeys only work against the browser's WebAuthn UI on the web surface —
// desktop shell, VS Code, and the Capacitor app never show the login screen.
const showPasskeySettings = isWebRuntime() && !isDesktopShell() && !isVSCode && !isCapacitorApp();
return (
<>
{showDesktopNetworkSettings && <DesktopNetworkSettings />}
{showPasskeySettings && <PasskeySettings />}
<AppLinkSecuritySettings />
{!isVSCode && <OpenCodeCliSettings />}
{!isVSCode && <OpenChamberToolsSettings />}
<OpenChamberVisualSettings visibleSettings={[
'fileEditorKeymap',
...(!isVSCode ? ['sessionTabs' as const] : []),
'autoSaveEnabled',
...(!isVSCode ? ['terminalQuickKeys' as const] : []),
...(!isVSCode ? ['terminalShell' as const] : []),
...(!isVSCode ? ['terminalLoginShell' as const] : []),
'messageTransport',
'reportUsage',
]} />
</>
);
};
// Visual section: Theme Mode, Font Size, Spacing, Input Bar Offset (mobile), Nav Rail
const VisualSectionContent: React.FC = () => {
const isVSCode = isVSCodeRuntime();
return <OpenChamberVisualSettings visibleSettings={[
'theme',
'windowControlsPosition',
'pwaInstallName',
'pwaOrientation',
'mobileKeyboardMode',
'timeFormat',
...(!isVSCode ? ['weekStart' as const] : []),
'fontSize',
'terminalFontSize',
'editorFontSize',
'spacing',
'scrollbars',
'inputBarOffset',
]} />;
};
// Chat section: User message rendering, Diff layout, Mobile status bar, Show reasoning traces, Follow-up behavior, Persist draft
const ChatSectionContent: React.FC = () => {
const isVSCode = isVSCodeRuntime();
return (
<OpenChamberVisualSettings
visibleSettings={[
'sessionGoal',
'sessionAssist',
'chatRenderMode',
'activityRenderMode',
'userMessageRendering',
'mermaidRendering',
'reasoning',
'showToolFileIcons',
'showTurnChangedFiles',
'expandedTools',
'collapsibleUserMessages',
'stickyUserHeader',
...(!isVSCode ? ['promptNavigatorEnabled' as const] : []),
'wideChatLayout',
'codeBlockLineWrap',
'splitAssistantMessageActions',
'subagentReadOnlyBanner',
'diffLayout',
'inputHistoryScope',
'inputHistoryLimit',
'dotfiles',
'fileViewerPreview',
'followUpBehavior',
'persistDraft',
'inputSpellcheck',
'largeTextPaste',
'enterToSend',
]}
/>
);
};
// Sessions section: Default model & agent, Session retention
const SessionsSectionContent: React.FC = () => {
return (
<>
<DefaultsSettings />
<SessionRetentionSettings />
</>
);
};
// Git section: Commit message model, Worktree settings
const GitSectionContent: React.FC = () => {
return <GitSettings />;
};
// Notifications section: Native browser notifications
const NotificationSectionContent: React.FC = () => {
return <NotificationSettings />;
};
// Voice section: Language selection and continuous mode
const VoiceSectionContent: React.FC = () => {
if (isVSCodeRuntime()) {
return null;
}
return <VoiceSettings />;
};
const TunnelSectionContent: React.FC = () => {
if (isVSCodeRuntime()) {
return null;
}
return <TunnelSettings />;
};