feat: improve mobile UX (#1591)

Added a mobile MCP overlay so MCP tools can be opened and managed from the mobile UI without relying on desktop-only dropdown behavior.
Improved mobile session panel touch handling so tapping the status/session area opens the right panel reliably on phones and tablets.
Cleaned up mobile usage provider metadata by removing duplicate rows, hiding unset providers, and showing provider logos consistently.
Added eager loading for provider logos used in mobile usage views to avoid delayed or missing icons when the panel opens.
Refined the mobile update and about flows in OpenChamber settings so release/update information is easier to read on small screens.
Adjusted related layout, header, VS Code layout, command palette, and settings text/localization details needed for the mobile polish.
This commit is contained in:
Bohdan Triapitsyn
2026-06-10 12:00:10 +03:00
committed by GitHub
parent 0153f8787d
commit eff6f46ad9
56 changed files with 1145 additions and 353 deletions
@@ -35,11 +35,13 @@ import { SnippetsPage } from '@/components/sections/snippets/SnippetsPage';
import { GitPage } from '@/components/sections/git-identities/GitPage';
import type { OpenChamberSection } from '@/components/sections/openchamber/types';
import { OpenChamberPage } from '@/components/sections/openchamber/OpenChamberPage';
import { AboutSettings } from '@/components/sections/openchamber/AboutSettings';
import { useDeviceInfo } from '@/lib/device';
import { isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
import { useI18n } from '@/lib/i18n';
import { Icon } from "@/components/icon/Icon";
import type { IconName } from "@/components/icon/icons";
import { McpIcon } from '@/components/icons/McpIcon';
import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore';
import {
SETTINGS_PAGE_METADATA,
@@ -74,6 +76,7 @@ interface SettingsViewProps {
isWindowed?: boolean;
/** Restrict top-level settings navigation to a specific product surface. */
visiblePageSlugs?: SettingsPageSlug[];
initialMobileStage?: MobileStage;
}
const pageOrder: SettingsPageSlug[] = [
@@ -98,6 +101,7 @@ const pageOrder: SettingsPageSlug[] = [
'skills.catalog',
'voice',
'tunnel',
'about',
];
const SNIPPETS_SETTINGS_ICON = { icon: 'chat-thread' } as const;
@@ -177,7 +181,7 @@ export function getSettingsNavIcon(slug: SettingsPageSlug): IconName | null {
case 'commands':
return 'slash-commands-2';
case 'mcp':
return 'plug-2';
return null;
case 'plugins':
return 'code-box';
@@ -195,6 +199,8 @@ export function getSettingsNavIcon(slug: SettingsPageSlug): IconName | null {
return 'mic';
case 'tunnel':
return 'global';
case 'about':
return 'information';
case 'home':
return null;
default:
@@ -278,7 +284,7 @@ const SettingsHome: React.FC<{ onOpen: (slug: SettingsPageSlug) => void }> = ({
);
};
export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile, isWindowed, visiblePageSlugs }) => {
export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile, isWindowed, visiblePageSlugs, initialMobileStage = 'nav' }) => {
const { t } = useI18n();
const deviceInfo = useDeviceInfo();
const isMobile = forceMobile ?? deviceInfo.isMobile;
@@ -288,7 +294,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
const setSettingsPage = useUIStore((state) => state.setSettingsPage);
const settingsSlug = resolveSettingsSlug(settingsPageRaw);
const [mobileStage, setMobileStage] = React.useState<MobileStage>('nav');
const [mobileStage, setMobileStage] = React.useState<MobileStage>(initialMobileStage);
const autoNavSlugRef = React.useRef<string | null>(null);
const [navWidth, setNavWidth] = React.useState(216);
@@ -492,6 +498,8 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
return t('settings.page.voice.title');
case 'tunnel':
return t('settings.page.tunnel.title');
case 'about':
return t('settings.page.about.title');
case 'home':
default:
return t('settings.view.home.title');
@@ -567,6 +575,8 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
return <ProvidersPage />;
case 'usage':
return <UsagePage />;
case 'about':
return <div className="h-full overflow-auto px-5 py-6"><AboutSettings /></div>;
case 'magic-prompts':
return <MagicPromptsPage />;
case 'snippets':
@@ -703,7 +713,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
{sortedFilteredPages.map((page) => {
const selected = settingsSlug === page.slug;
const iconName = getSettingsNavIcon(page.slug);
if (!iconName) return null;
if (!iconName && page.slug !== 'mcp') return null;
return (
<Tooltip key={page.slug}>
@@ -719,7 +729,9 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
: 'text-foreground hover:bg-interactive-hover'
)}
>
<Icon name={iconName} className="h-4 w-4 shrink-0" />
{page.slug === 'mcp'
? <McpIcon className="h-4 w-4 shrink-0" />
: <Icon name={iconName!} className="h-4 w-4 shrink-0" />}
<span className="flex items-center gap-1.5 whitespace-nowrap overflow-hidden transition-opacity duration-150 opacity-100">
<span className="typography-ui-label font-normal truncate">{getPageTitle(page.slug)}</span>
{(page.slug === 'voice' || page.slug === 'tunnel') && (