feat: redesign settings pages to match canonical flat UI patterns (#493)
* refactor(settings): new IA shell + projects section + skills catalog discoverability * chore(settings): split providers list by scope; show user before project * fix: navigation flow in mobile Settings * feat: redesign settings pages to use modern elevated surface patterns * feat: replace helper text with tooltips in settings * ui: redesign update dialog and fix external link routing - Restructures UpdateDialog to focus on changelog readability with a wider max-w-4xl canvas - Highlights @username contributor mentions with theme primary color - Strips excessive vertical padding and right-aligns compact action buttons - Disables streamdown's internal link safety dialog in favor of direct Tauri shell routing * feat: refactor Git identities into dedicated Git settings page * feat: unify sidebar background styling across VS Code and web/mobile * fix: adjust button styling and layout for mobile settings pages * feat: add MCP settings page and sidebar * feat: hide models in provider view (thanks to @nguyenngothuong) * feat: add "Add new provider" option to model selector dropdown * fix: local evroc logo + provider dropdown icons * fix: increase width of provider menu * fix: dark theme background color for better contrast * feat: update @opencode-ai/sdk dependency to v1.2.10 * fix: restore session sorting to only use updated time * fix: added settings for sessions deletion dialog * fix: adjust padding on settings pages for better layout * fix: standardize select dropdown height across UI * fix: agent selector UI and notification settings * fix: remove redundant helper text from settings pages * fix: update UI layout for description fields * fix: remove border-none and shadow-none from textarea classes * fix: enable context menu on sidebar items * feat: refactor UI controls and layout patterns across settings pages * fix: use headerless blocks when page title already provides context * fix: remove subtask option from command settings * fix: refactor mcp page settings * fix: reduce spacing in skills configuration pages * feat: refactor voice settings * feat: refactor settings sidebar sections
This commit is contained in:
committed by
GitHub
parent
d2d39c48ac
commit
d2358c2c03
@@ -15,7 +15,13 @@ export interface ModelListItem {
|
||||
|
||||
export const useModelLists = () => {
|
||||
const { providers } = useConfigStore();
|
||||
const { favoriteModels, recentModels } = useUIStore();
|
||||
const favoriteModels = useUIStore((state) => state.favoriteModels);
|
||||
const recentModels = useUIStore((state) => state.recentModels);
|
||||
const hiddenModels = useUIStore((state) => state.hiddenModels);
|
||||
|
||||
const isHidden = React.useCallback((providerID: string, modelID: string) => {
|
||||
return hiddenModels.some((item) => item.providerID === providerID && item.modelID === modelID);
|
||||
}, [hiddenModels]);
|
||||
|
||||
const favoriteModelsList = React.useMemo(() => {
|
||||
return favoriteModels
|
||||
@@ -25,10 +31,11 @@ export const useModelLists = () => {
|
||||
const providerModels = Array.isArray(provider.models) ? provider.models : [];
|
||||
const model = providerModels.find((m: ProviderModel) => m.id === modelID);
|
||||
if (!model) return null;
|
||||
if (isHidden(providerID, modelID)) return null;
|
||||
return { provider, model, providerID, modelID };
|
||||
})
|
||||
.filter((item): item is ModelListItem => item !== null);
|
||||
}, [favoriteModels, providers]);
|
||||
}, [favoriteModels, providers, isHidden]);
|
||||
|
||||
const recentModelsList = React.useMemo(() => {
|
||||
return recentModels
|
||||
@@ -38,13 +45,14 @@ export const useModelLists = () => {
|
||||
const providerModels = Array.isArray(provider.models) ? provider.models : [];
|
||||
const model = providerModels.find((m: ProviderModel) => m.id === modelID);
|
||||
if (!model) return null;
|
||||
if (isHidden(providerID, modelID)) return null;
|
||||
return { provider, model, providerID, modelID };
|
||||
})
|
||||
.filter((item): item is ModelListItem => item !== null)
|
||||
.filter(({ providerID, modelID }) =>
|
||||
!favoriteModels.some(fav => fav.providerID === providerID && fav.modelID === modelID)
|
||||
);
|
||||
}, [recentModels, providers, favoriteModels]);
|
||||
}, [recentModels, providers, favoriteModels, isHidden]);
|
||||
|
||||
return { favoriteModelsList, recentModelsList };
|
||||
};
|
||||
|
||||
@@ -17,9 +17,36 @@ const LOCAL_PROVIDER_LOGO_MAP = new Map<string, string>();
|
||||
|
||||
const LOGO_ALIAS = new Map<string, string>([
|
||||
['codex', 'openai'],
|
||||
['chatgpt', 'openai'],
|
||||
['claude', 'anthropic'],
|
||||
['gemini', 'google'],
|
||||
['evroc-ai', 'evroc'],
|
||||
['evrocai', 'evroc'],
|
||||
]);
|
||||
|
||||
const normalizeProviderId = (providerId: string | null | undefined) => {
|
||||
return (providerId ?? '')
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/^models\./, '')
|
||||
.replace(/^provider\./, '')
|
||||
.replace(/\s+/g, '-');
|
||||
};
|
||||
|
||||
const buildLogoCandidates = (providerId: string | null | undefined) => {
|
||||
const normalized = normalizeProviderId(providerId);
|
||||
if (!normalized) {
|
||||
return [] as string[];
|
||||
}
|
||||
|
||||
const compact = normalized.replace(/[^a-z0-9_\-./:]/g, '');
|
||||
const primary = compact.split(/[/:]/)[0] || compact;
|
||||
const candidates = [compact, primary, LOGO_ALIAS.get(compact), LOGO_ALIAS.get(primary)]
|
||||
.filter((value): value is string => Boolean(value && value.length > 0));
|
||||
|
||||
return [...new Set(candidates)];
|
||||
};
|
||||
|
||||
for (const [path, url] of Object.entries(localLogoModules)) {
|
||||
const match = path.match(/provider-logos\/([^/]+)\.svg$/i);
|
||||
if (match?.[1] && url) {
|
||||
@@ -28,22 +55,23 @@ for (const [path, url] of Object.entries(localLogoModules)) {
|
||||
}
|
||||
|
||||
export function useProviderLogo(providerId: string | null | undefined): UseProviderLogoReturn {
|
||||
const normalizedId = providerId?.toLowerCase() ?? null;
|
||||
const resolvedId = normalizedId ? LOGO_ALIAS.get(normalizedId) ?? normalizedId : null;
|
||||
const hasLocalLogo = resolvedId ? LOCAL_PROVIDER_LOGO_MAP.has(resolvedId) : false;
|
||||
const localLogoSrc = resolvedId ? LOCAL_PROVIDER_LOGO_MAP.get(resolvedId) ?? null : null;
|
||||
const candidates = buildLogoCandidates(providerId);
|
||||
const localResolvedId = candidates.find((candidate) => LOCAL_PROVIDER_LOGO_MAP.has(candidate)) ?? null;
|
||||
const remoteResolvedId = candidates[0] ?? null;
|
||||
const hasLocalLogo = Boolean(localResolvedId);
|
||||
const localLogoSrc = localResolvedId ? LOCAL_PROVIDER_LOGO_MAP.get(localResolvedId) ?? null : null;
|
||||
|
||||
const [source, setSource] = useState<LogoSource>(hasLocalLogo ? 'local' : 'remote');
|
||||
|
||||
useEffect(() => {
|
||||
setSource(hasLocalLogo ? 'local' : 'remote');
|
||||
}, [hasLocalLogo, resolvedId]);
|
||||
}, [hasLocalLogo, localResolvedId, remoteResolvedId]);
|
||||
|
||||
const handleError = useCallback(() => {
|
||||
setSource((current) => (current === 'local' && hasLocalLogo ? 'remote' : 'none'));
|
||||
}, [hasLocalLogo]);
|
||||
|
||||
if (!resolvedId) {
|
||||
if (!localResolvedId && !remoteResolvedId) {
|
||||
return { src: null, onError: handleError, hasLogo: false };
|
||||
}
|
||||
|
||||
@@ -55,9 +83,9 @@ export function useProviderLogo(providerId: string | null | undefined): UseProvi
|
||||
};
|
||||
}
|
||||
|
||||
if (source === 'remote') {
|
||||
if (source === 'remote' && remoteResolvedId) {
|
||||
return {
|
||||
src: `https://models.dev/logos/${resolvedId}.svg`,
|
||||
src: `https://models.dev/logos/${remoteResolvedId}.svg`,
|
||||
onError: handleError,
|
||||
hasLogo: true,
|
||||
};
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useSessionStore } from '@/stores/useSessionStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { parseRoute, updateBrowserURL, hasRouteParams } from '@/lib/router';
|
||||
import type { RouteState, AppRouteState } from '@/lib/router';
|
||||
import type { SidebarSection } from '@/constants/sidebar';
|
||||
import type { MainTab } from '@/stores/useUIStore';
|
||||
import { resolveSettingsSlug } from '@/lib/settings/metadata';
|
||||
|
||||
/**
|
||||
* Check if running in VS Code webview context.
|
||||
@@ -41,7 +41,7 @@ export function useRouter(): void {
|
||||
const setCurrentSession = useSessionStore((state) => state.setCurrentSession);
|
||||
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
|
||||
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
|
||||
const setSidebarSection = useUIStore((state) => state.setSidebarSection);
|
||||
const setSettingsPage = useUIStore((state) => state.setSettingsPage);
|
||||
const navigateToDiff = useUIStore((state) => state.navigateToDiff);
|
||||
|
||||
/**
|
||||
@@ -65,8 +65,8 @@ export function useRouter(): void {
|
||||
}
|
||||
|
||||
// 2. Handle settings (takes precedence over tabs - it's a full-screen overlay)
|
||||
if (route.settingsSection) {
|
||||
setSidebarSection(route.settingsSection);
|
||||
if (route.settingsPath) {
|
||||
setSettingsPage(resolveSettingsSlug(route.settingsPath));
|
||||
setSettingsDialogOpen(true);
|
||||
// Don't process tab when settings is open
|
||||
return;
|
||||
@@ -90,7 +90,7 @@ export function useRouter(): void {
|
||||
isApplyingRouteRef.current = false;
|
||||
}
|
||||
},
|
||||
[setCurrentSession, setActiveMainTab, setSettingsDialogOpen, setSidebarSection, navigateToDiff]
|
||||
[setCurrentSession, setActiveMainTab, setSettingsDialogOpen, setSettingsPage, navigateToDiff]
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -104,7 +104,7 @@ export function useRouter(): void {
|
||||
sessionId: sessionState.currentSessionId,
|
||||
tab: uiState.activeMainTab,
|
||||
isSettingsOpen: uiState.isSettingsDialogOpen,
|
||||
settingsSection: uiState.sidebarSection,
|
||||
settingsPath: uiState.settingsPage,
|
||||
diffFile: uiState.pendingDiffFile,
|
||||
};
|
||||
}, []);
|
||||
@@ -183,7 +183,7 @@ export function useRouter(): void {
|
||||
|
||||
let prevTab: MainTab = useUIStore.getState().activeMainTab;
|
||||
let prevSettingsOpen: boolean = useUIStore.getState().isSettingsDialogOpen;
|
||||
let prevSettingsSection: SidebarSection = useUIStore.getState().sidebarSection;
|
||||
let prevSettingsPath: string = useUIStore.getState().settingsPage;
|
||||
let prevDiffFile: string | null = useUIStore.getState().pendingDiffFile;
|
||||
|
||||
const unsubscribe = useUIStore.subscribe((state) => {
|
||||
@@ -194,17 +194,17 @@ export function useRouter(): void {
|
||||
|
||||
const tabChanged = state.activeMainTab !== prevTab;
|
||||
const settingsOpenChanged = state.isSettingsDialogOpen !== prevSettingsOpen;
|
||||
const settingsSectionChanged = state.sidebarSection !== prevSettingsSection;
|
||||
const settingsPathChanged = state.settingsPage !== prevSettingsPath;
|
||||
const diffFileChanged = state.pendingDiffFile !== prevDiffFile && state.activeMainTab === 'diff';
|
||||
|
||||
// Update tracking vars
|
||||
prevTab = state.activeMainTab;
|
||||
prevSettingsOpen = state.isSettingsDialogOpen;
|
||||
prevSettingsSection = state.sidebarSection;
|
||||
prevSettingsPath = state.settingsPage;
|
||||
prevDiffFile = state.pendingDiffFile;
|
||||
|
||||
// Only sync if something relevant changed
|
||||
if (tabChanged || settingsOpenChanged || settingsSectionChanged || diffFileChanged) {
|
||||
if (tabChanged || settingsOpenChanged || settingsPathChanged || diffFileChanged) {
|
||||
syncURLFromState();
|
||||
}
|
||||
});
|
||||
@@ -263,8 +263,8 @@ export function navigateToRoute(route: Partial<RouteState>): void {
|
||||
if (route.sessionId) {
|
||||
void useSessionStore.getState().setCurrentSession(route.sessionId);
|
||||
}
|
||||
if (route.settingsSection) {
|
||||
useUIStore.getState().setSidebarSection(route.settingsSection);
|
||||
if (route.settingsPath) {
|
||||
useUIStore.getState().setSettingsPage(resolveSettingsSlug(route.settingsPath));
|
||||
useUIStore.getState().setSettingsDialogOpen(true);
|
||||
} else if (route.tab) {
|
||||
useUIStore.getState().setActiveMainTab(route.tab);
|
||||
@@ -281,8 +281,8 @@ export function navigateToRoute(route: Partial<RouteState>): void {
|
||||
if (route.sessionId) {
|
||||
params.set('session', route.sessionId);
|
||||
}
|
||||
if (route.settingsSection) {
|
||||
params.set('settings', route.settingsSection);
|
||||
if (route.settingsPath) {
|
||||
params.set('settings', route.settingsPath);
|
||||
} else if (route.tab && route.tab !== 'chat') {
|
||||
if (useUIStore.getState().isSettingsDialogOpen) {
|
||||
useUIStore.getState().setSettingsDialogOpen(false);
|
||||
@@ -302,8 +302,8 @@ export function navigateToRoute(route: Partial<RouteState>): void {
|
||||
if (route.sessionId) {
|
||||
void useSessionStore.getState().setCurrentSession(route.sessionId);
|
||||
}
|
||||
if (route.settingsSection) {
|
||||
useUIStore.getState().setSidebarSection(route.settingsSection);
|
||||
if (route.settingsPath) {
|
||||
useUIStore.getState().setSettingsPage(resolveSettingsSlug(route.settingsPath));
|
||||
useUIStore.getState().setSettingsDialogOpen(true);
|
||||
} else if (route.tab) {
|
||||
useUIStore.getState().setActiveMainTab(route.tab);
|
||||
@@ -331,8 +331,7 @@ export function getShareableURL(): string {
|
||||
}
|
||||
|
||||
if (uiState.isSettingsDialogOpen) {
|
||||
const settingsSection = uiState.sidebarSection === 'sessions' ? 'settings' : uiState.sidebarSection;
|
||||
params.set('settings', settingsSection);
|
||||
params.set('settings', uiState.settingsPage || 'home');
|
||||
} else if (uiState.activeMainTab !== 'chat') {
|
||||
params.set('tab', uiState.activeMainTab);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user