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:
Bohdan Triapitsyn
2026-02-24 03:28:30 +02:00
committed by GitHub
parent d2d39c48ac
commit d2358c2c03
90 changed files with 8062 additions and 7128 deletions
+11 -3
View File
@@ -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 };
};
+36 -8
View File
@@ -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,
};
+17 -18
View File
@@ -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);
}