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:
committed by
GitHub
parent
0153f8787d
commit
eff6f46ad9
@@ -37,6 +37,7 @@ import { UpdateDialog } from '@/components/ui/UpdateDialog';
|
||||
import { useDeviceInfo, useTabletStandalonePwaRuntime } from '@/lib/device';
|
||||
import { cn, hasModifier } from '@/lib/utils';
|
||||
import { McpDropdownContent } from '@/components/mcp/McpDropdown';
|
||||
import { McpIcon } from '@/components/icons/McpIcon';
|
||||
import { ProviderLogo } from '@/components/ui/ProviderLogo';
|
||||
import { formatQuotaValueLabel, formatQuotaResetLabel, formatWindowLabel, QUOTA_PROVIDERS, calculatePace, calculateExpectedUsagePercent } from '@/lib/quota';
|
||||
import { UsageProgressBar } from '@/components/sections/usage/UsageProgressBar';
|
||||
@@ -1776,13 +1777,13 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
}, [activeMainTab, isMobile, setActiveMainTab]);
|
||||
|
||||
const servicesTabs = React.useMemo(() => {
|
||||
const base: Array<{ value: 'instance' | 'usage' | 'mcp'; label: string; icon: IconName }> = [];
|
||||
const base: Array<{ value: 'instance' | 'usage' | 'mcp'; label: string; icon: React.ReactNode }> = [];
|
||||
if (isDesktopApp) {
|
||||
base.push({ value: 'instance', label: t('layout.services.instance'), icon: "server" });
|
||||
base.push({ value: 'instance', label: t('layout.services.instance'), icon: <Icon name="server" className="h-3.5 w-3.5" /> });
|
||||
}
|
||||
base.push(
|
||||
{ value: 'usage', label: t('layout.services.usage'), icon: "timer" },
|
||||
{ value: 'mcp', label: 'MCP', icon: "plug-2" }
|
||||
{ value: 'usage', label: t('layout.services.usage'), icon: <Icon name="timer" className="h-3.5 w-3.5" /> },
|
||||
{ value: 'mcp', label: 'MCP', icon: <McpIcon className="h-3.5 w-3.5" /> }
|
||||
);
|
||||
return base;
|
||||
}, [isDesktopApp, t]);
|
||||
@@ -1791,7 +1792,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
return servicesTabs.map((tab) => ({
|
||||
id: tab.value,
|
||||
label: tab.label,
|
||||
icon: <Icon name={tab.icon} className="h-3.5 w-3.5" />,
|
||||
icon: tab.icon,
|
||||
}));
|
||||
}, [servicesTabs]);
|
||||
|
||||
@@ -1866,7 +1867,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
const mobileServicesTabItems = React.useMemo<SortableTabsStripItem[]>(() => {
|
||||
return [
|
||||
{ id: 'usage', label: t('layout.services.usage'), icon: <Icon name="timer" className="h-3.5 w-3.5" /> },
|
||||
{ id: 'mcp', label: 'MCP', icon: <Icon name="command" className="h-3.5 w-3.5" /> },
|
||||
{ id: 'mcp', label: 'MCP', icon: <McpIcon className="h-3.5 w-3.5" /> },
|
||||
];
|
||||
}, [t]);
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import { DrawerProvider } from '@/contexts/DrawerContext';
|
||||
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { useUpdateStore } from '@/stores/useUpdateStore';
|
||||
import { useUpdatePolling } from '@/hooks/useUpdatePolling';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
|
||||
@@ -219,41 +219,7 @@ export const MainLayout: React.FC = () => {
|
||||
}
|
||||
}, [isMobile, isSettingsDialogOpen, isRightSidebarOpen, setMobileSessionPanelOpen, setRightSidebarOpen]);
|
||||
|
||||
// Trigger initial update check shortly after mount, then repeat using server-suggested cadence.
|
||||
const checkForUpdates = useUpdateStore((state) => state.checkForUpdates);
|
||||
React.useEffect(() => {
|
||||
const initialDelayMs = 3000;
|
||||
const defaultIntervalMs = 60 * 60 * 1000;
|
||||
const minIntervalMs = 5 * 60 * 1000;
|
||||
const maxIntervalMs = 24 * 60 * 60 * 1000;
|
||||
let disposed = false;
|
||||
let timer: number | null = null;
|
||||
|
||||
const clampIntervalMs = (seconds: number): number => {
|
||||
const ms = Math.round(seconds * 1000);
|
||||
return Math.max(minIntervalMs, Math.min(maxIntervalMs, ms));
|
||||
};
|
||||
|
||||
const scheduleNext = (delayMs: number) => {
|
||||
if (disposed) return;
|
||||
timer = window.setTimeout(async () => {
|
||||
const suggestedSec = await checkForUpdates();
|
||||
const nextDelay = typeof suggestedSec === 'number' && Number.isFinite(suggestedSec)
|
||||
? clampIntervalMs(suggestedSec)
|
||||
: defaultIntervalMs;
|
||||
scheduleNext(nextDelay);
|
||||
}, delayMs);
|
||||
};
|
||||
|
||||
scheduleNext(initialDelayMs);
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
if (timer !== null) {
|
||||
window.clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
}, [checkForUpdates]);
|
||||
useUpdatePolling();
|
||||
|
||||
React.useEffect(() => {
|
||||
const previous = useUIStore.getState().isMobile;
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
import { useUpdatePolling } from '@/hooks/useUpdatePolling';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { toast } from '@/components/ui';
|
||||
import { ProviderLogo } from '@/components/ui/ProviderLogo';
|
||||
@@ -31,7 +32,6 @@ import { PaceIndicator } from '@/components/sections/usage/PaceIndicator';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import { formatQuotaValueLabel, formatQuotaResetLabel, formatWindowLabel, QUOTA_PROVIDERS, calculatePace, calculateExpectedUsagePercent } from '@/lib/quota';
|
||||
import { useQuotaAutoRefresh, useQuotaStore } from '@/stores/useQuotaStore';
|
||||
import { useUpdateStore } from '@/stores/useUpdateStore';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
import { formatTimeForPreference } from '@/lib/timeFormat';
|
||||
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
|
||||
@@ -73,41 +73,7 @@ type VSCodeView = 'sessions' | 'chat' | 'settings';
|
||||
export const VSCodeLayout: React.FC = () => {
|
||||
const { t } = useI18n();
|
||||
const runtimeApis = useRuntimeAPIs();
|
||||
const checkForUpdates = useUpdateStore((state) => state.checkForUpdates);
|
||||
|
||||
React.useEffect(() => {
|
||||
const initialDelayMs = 3000;
|
||||
const defaultIntervalMs = 60 * 60 * 1000;
|
||||
const minIntervalMs = 5 * 60 * 1000;
|
||||
const maxIntervalMs = 24 * 60 * 60 * 1000;
|
||||
let disposed = false;
|
||||
let timer: number | null = null;
|
||||
|
||||
const clampIntervalMs = (seconds: number): number => {
|
||||
const ms = Math.round(seconds * 1000);
|
||||
return Math.max(minIntervalMs, Math.min(maxIntervalMs, ms));
|
||||
};
|
||||
|
||||
const scheduleNext = (delayMs: number) => {
|
||||
if (disposed) return;
|
||||
timer = window.setTimeout(async () => {
|
||||
const suggestedSec = await checkForUpdates();
|
||||
const nextDelay = typeof suggestedSec === 'number' && Number.isFinite(suggestedSec)
|
||||
? clampIntervalMs(suggestedSec)
|
||||
: defaultIntervalMs;
|
||||
scheduleNext(nextDelay);
|
||||
}, delayMs);
|
||||
};
|
||||
|
||||
scheduleNext(initialDelayMs);
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
if (timer !== null) {
|
||||
window.clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
}, [checkForUpdates]);
|
||||
useUpdatePolling();
|
||||
|
||||
const viewMode = React.useMemo<'sidebar' | 'editor'>(() => {
|
||||
const configured =
|
||||
|
||||
Reference in New Issue
Block a user