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
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
Reference in New Issue
Block a user