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
+34
View File
@@ -14,6 +14,7 @@ const localLogoModules = import.meta.glob<string>('../assets/provider-logos/*.sv
});
const LOCAL_PROVIDER_LOGO_MAP = new Map<string, string>();
const PRELOADED_LOGO_SRCS = new Set<string>();
const LOGO_ALIAS = new Map<string, string>([
['codex', 'openai'],
@@ -50,6 +51,39 @@ const buildLogoCandidates = (providerId: string | null | undefined) => {
return [...new Set(candidates)];
};
const resolveProviderLogoSrc = (providerId: string | null | undefined): string | null => {
const candidates = buildLogoCandidates(providerId);
const localResolvedId = candidates.find((candidate) => LOCAL_PROVIDER_LOGO_MAP.has(candidate)) ?? null;
const localLogoSrc = localResolvedId ? LOCAL_PROVIDER_LOGO_MAP.get(localResolvedId) ?? null : null;
if (localLogoSrc) {
return localLogoSrc;
}
const remoteResolvedId = candidates[0] ?? null;
return remoteResolvedId ? `https://models.dev/logos/${remoteResolvedId}.svg` : null;
};
export const preloadProviderLogo = (providerId: string | null | undefined): void => {
if (typeof Image === 'undefined') return;
const src = resolveProviderLogoSrc(providerId);
if (!src || PRELOADED_LOGO_SRCS.has(src)) return;
PRELOADED_LOGO_SRCS.add(src);
const image = new Image();
image.decoding = 'async';
image.onerror = () => {
PRELOADED_LOGO_SRCS.delete(src);
};
image.src = src;
void image.decode?.().catch(() => undefined);
};
export const preloadProviderLogos = (providerIds: readonly (string | null | undefined)[]): void => {
for (const providerId of providerIds) {
preloadProviderLogo(providerId);
}
};
for (const [path, url] of Object.entries(localLogoModules)) {
const match = path.match(/provider-logos\/([^/]+)\.svg$/i);
if (match?.[1] && url) {
+46
View File
@@ -0,0 +1,46 @@
import React from 'react';
import { useUpdateStore } from '@/stores/useUpdateStore';
export function useUpdatePolling() {
const checkForUpdates = useUpdateStore((state) => state.checkForUpdates);
const checkForUpdatesRef = React.useRef(checkForUpdates);
React.useEffect(() => {
checkForUpdatesRef.current = checkForUpdates;
}, [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 checkForUpdatesRef.current();
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);
}
};
}, []);
}