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
+1
View File
@@ -511,6 +511,7 @@ export interface SettingsPayload {
pinnedDirectories?: string[];
showReasoningTraces?: boolean;
showTextJustificationActivity?: boolean;
showDeletionDialog?: boolean;
nativeNotificationsEnabled?: boolean;
notificationMode?: 'always' | 'hidden-only';
autoDeleteEnabled?: boolean;
@@ -5,6 +5,7 @@ import type { DesktopSettings } from '@/lib/desktop';
type AppearanceSlice = {
showReasoningTraces: boolean;
showTextJustificationActivity: boolean;
showDeletionDialog: boolean;
nativeNotificationsEnabled: boolean;
notificationMode: 'always' | 'hidden-only';
notifyOnSubtasks: boolean;
@@ -45,6 +46,7 @@ export const startAppearanceAutoSave = (): void => {
let previous: AppearanceSlice = {
showReasoningTraces: useUIStore.getState().showReasoningTraces,
showTextJustificationActivity: useUIStore.getState().showTextJustificationActivity,
showDeletionDialog: useUIStore.getState().showDeletionDialog,
nativeNotificationsEnabled: useUIStore.getState().nativeNotificationsEnabled,
notificationMode: useUIStore.getState().notificationMode,
notifyOnSubtasks: useUIStore.getState().notifyOnSubtasks,
@@ -92,6 +94,7 @@ export const startAppearanceAutoSave = (): void => {
const current: AppearanceSlice = {
showReasoningTraces: state.showReasoningTraces,
showTextJustificationActivity: state.showTextJustificationActivity,
showDeletionDialog: state.showDeletionDialog,
nativeNotificationsEnabled: state.nativeNotificationsEnabled,
notificationMode: state.notificationMode,
notifyOnSubtasks: state.notifyOnSubtasks,
@@ -123,6 +126,9 @@ export const startAppearanceAutoSave = (): void => {
if (current.showTextJustificationActivity !== previous.showTextJustificationActivity) {
diff.showTextJustificationActivity = current.showTextJustificationActivity;
}
if (current.showDeletionDialog !== previous.showDeletionDialog) {
diff.showDeletionDialog = current.showDeletionDialog;
}
if (current.nativeNotificationsEnabled !== previous.nativeNotificationsEnabled) {
diff.nativeNotificationsEnabled = current.nativeNotificationsEnabled;
}
+1
View File
@@ -46,6 +46,7 @@ export type DesktopSettings = {
pinnedDirectories?: string[];
showReasoningTraces?: boolean;
showTextJustificationActivity?: boolean;
showDeletionDialog?: boolean;
nativeNotificationsEnabled?: boolean;
notificationMode?: 'always' | 'hidden-only';
notifyOnSubtasks?: boolean;
+1 -2
View File
@@ -1806,7 +1806,7 @@ class OpencodeService {
}
}
async listCommandsWithDetails(): Promise<Array<{ name: string; description?: string; agent?: string; model?: string; template?: string; subtask?: boolean }>> {
async listCommandsWithDetails(): Promise<Array<{ name: string; description?: string; agent?: string; model?: string; template?: string }>> {
try {
const response = await this.client.command.list(
this.currentDirectory ? { directory: this.currentDirectory } : undefined
@@ -1818,7 +1818,6 @@ class OpencodeService {
agent: cmd.agent as string | undefined,
model: cmd.model as string | undefined,
template: cmd.template as string | undefined,
subtask: cmd.subtask as boolean | undefined
}));
} catch {
return [];
+6
View File
@@ -238,6 +238,9 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => {
if (typeof settings.showTextJustificationActivity === 'boolean' && settings.showTextJustificationActivity !== store.showTextJustificationActivity) {
store.setShowTextJustificationActivity(settings.showTextJustificationActivity);
}
if (typeof settings.showDeletionDialog === 'boolean' && settings.showDeletionDialog !== store.showDeletionDialog) {
store.setShowDeletionDialog(settings.showDeletionDialog);
}
if (typeof settings.nativeNotificationsEnabled === 'boolean' && settings.nativeNotificationsEnabled !== store.nativeNotificationsEnabled) {
store.setNativeNotificationsEnabled(settings.nativeNotificationsEnabled);
}
@@ -426,6 +429,9 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
if (typeof candidate.showTextJustificationActivity === 'boolean') {
result.showTextJustificationActivity = candidate.showTextJustificationActivity;
}
if (typeof candidate.showDeletionDialog === 'boolean') {
result.showDeletionDialog = candidate.showDeletionDialog;
}
if (typeof candidate.nativeNotificationsEnabled === 'boolean') {
result.nativeNotificationsEnabled = candidate.nativeNotificationsEnabled;
}
+13 -10
View File
@@ -1,4 +1,3 @@
import type { SidebarSection } from '@/constants/sidebar';
import type { MainTab } from '@/stores/useUIStore';
import {
type RouteState,
@@ -17,7 +16,7 @@ export function parseRoute(searchParams?: URLSearchParams): RouteState {
return {
sessionId: parseSessionId(params),
tab: parseTab(params),
settingsSection: parseSettingsSection(params),
settingsPath: parseSettingsPath(params),
diffFile: parseDiffFile(params),
};
}
@@ -68,10 +67,10 @@ function parseTab(params: URLSearchParams): MainTab | null {
}
/**
* Parse settings section from URL parameters.
* Returns null if missing or invalid.
* Parse settings target from URL parameters.
* Returns null if missing/empty.
*/
function parseSettingsSection(params: URLSearchParams): SidebarSection | null {
function parseSettingsPath(params: URLSearchParams): string | null {
const value = params.get(ROUTE_PARAMS.SETTINGS);
if (!value) {
return null;
@@ -79,17 +78,21 @@ function parseSettingsSection(params: URLSearchParams): SidebarSection | null {
const normalized = value.toLowerCase().trim();
// Check if it's a valid section
if ((VALID_SETTINGS_SECTIONS as readonly string[]).includes(normalized)) {
return normalized as SidebarSection;
if (normalized.length === 0) {
return null;
}
// Handle common aliases
if (normalized === 'openchamber' || normalized === 'general' || normalized === 'preferences') {
return 'settings';
return 'home';
}
return null;
// Keep legacy section ids as-is (mapping happens at apply time).
if ((VALID_SETTINGS_SECTIONS as readonly string[]).includes(normalized)) {
return normalized;
}
return normalized;
}
/**
+3 -4
View File
@@ -1,4 +1,3 @@
import type { SidebarSection } from '@/constants/sidebar';
import type { MainTab } from '@/stores/useUIStore';
import { ROUTE_PARAMS } from './types';
@@ -9,7 +8,7 @@ export interface AppRouteState {
sessionId: string | null;
tab: MainTab;
isSettingsOpen: boolean;
settingsSection: SidebarSection;
settingsPath: string;
diffFile: string | null;
}
@@ -32,8 +31,8 @@ export function serializeRoute(state: AppRouteState): URLSearchParams {
// Settings takes precedence - if open, include settings section
if (state.isSettingsOpen) {
const settingsSection = state.settingsSection === 'sessions' ? 'settings' : state.settingsSection;
params.set(ROUTE_PARAMS.SETTINGS, settingsSection);
const settingsPath = state.settingsPath.trim().length > 0 ? state.settingsPath : 'home';
params.set(ROUTE_PARAMS.SETTINGS, settingsPath);
// Don't include tab when settings is open (it's a full-screen overlay)
return params;
}
+2 -1
View File
@@ -11,7 +11,7 @@ export interface RouteState {
/** Main tab to display (chat, git, diff, terminal, files) */
tab: MainTab | null;
/** Settings section - when non-null, settings dialog should be open */
settingsSection: SidebarSection | null;
settingsPath: string | null;
/** File path for diff view */
diffFile: string | null;
}
@@ -40,6 +40,7 @@ export const VALID_SETTINGS_SECTIONS: readonly SidebarSection[] = [
'commands',
'skills',
'providers',
'usage',
'git-identities',
] as const;
+198
View File
@@ -0,0 +1,198 @@
import type { SidebarSection } from '@/constants/sidebar';
export type SettingsPageSlug =
| 'home'
| 'projects'
| 'providers'
| 'usage'
| 'agents'
| 'commands'
| 'mcp'
| 'skills.installed'
| 'skills.catalog'
| 'git'
| 'appearance'
| 'chat'
| 'shortcuts'
| 'sessions'
| 'notifications'
| 'voice';
export type SettingsPageGroup =
| 'appearance'
| 'projects'
| 'general'
| 'opencode'
| 'git'
| 'skills'
| 'usage'
| 'advanced';
export interface SettingsRuntimeContext {
isVSCode: boolean;
isWeb: boolean;
isDesktop: boolean;
}
export interface SettingsPageMeta {
slug: SettingsPageSlug;
title: string;
group: SettingsPageGroup;
kind: 'single' | 'split';
description?: string;
keywords?: string[];
isAvailable?: (ctx: SettingsRuntimeContext) => boolean;
}
export const SETTINGS_GROUP_LABELS: Record<SettingsPageGroup, string> = {
appearance: 'Appearance',
projects: 'Projects',
general: 'General',
opencode: 'OpenCode',
git: 'Git',
skills: 'Skills',
usage: 'Usage',
advanced: 'Advanced',
};
export const SETTINGS_PAGE_METADATA: readonly SettingsPageMeta[] = [
{
slug: 'home',
title: 'Settings',
group: 'general',
kind: 'single',
description: 'Search and jump to common pages.',
keywords: ['search', 'settings'],
},
{
slug: 'projects',
title: 'Projects',
group: 'projects',
kind: 'split',
keywords: ['project', 'projects', 'worktree', 'worktrees', 'repo', 'repository', 'directory'],
},
{
slug: 'providers',
title: 'Providers',
group: 'opencode',
kind: 'split',
keywords: ['provider', 'providers', 'models', 'model', 'api key', 'api keys', 'openai', 'anthropic', 'ollama', 'credentials'],
},
{
slug: 'usage',
title: 'Usage',
group: 'usage',
kind: 'split',
keywords: ['quota', 'billing', 'tokens', 'usage', 'limits'],
},
{
slug: 'agents',
title: 'Agents',
group: 'opencode',
kind: 'split',
keywords: ['agent', 'agents', 'prompts', 'tools', 'permissions'],
},
{
slug: 'commands',
title: 'Commands',
group: 'opencode',
kind: 'split',
keywords: ['command', 'commands', 'slash', 'macros', 'automation'],
},
{
slug: 'mcp',
title: 'MCP',
group: 'opencode',
kind: 'split',
keywords: ['mcp', 'model context protocol', 'servers', 'tools', 'remote', 'stdio'],
},
{
slug: 'skills.installed',
title: 'Skills',
group: 'skills',
kind: 'split',
keywords: ['skill', 'skills', 'instructions', 'install', 'catalog'],
},
{
slug: 'skills.catalog',
title: 'Skills Catalog',
group: 'skills',
kind: 'single',
keywords: ['install', 'catalog', 'external', 'repository', 'skills catalog'],
},
{
slug: 'git',
title: 'Git',
group: 'git',
kind: 'single',
keywords: ['git', 'github', 'identity', 'identities', 'ssh', 'profiles', 'credentials', 'keys', 'commit', 'gitmoji', 'oauth', 'prs', 'issues'],
isAvailable: (ctx) => !ctx.isVSCode,
},
{
slug: 'appearance',
title: 'Appearance',
group: 'appearance',
kind: 'single',
keywords: ['theme', 'font', 'spacing', 'padding', 'corner radius', 'radius', 'input bar', 'terminal'],
},
{
slug: 'chat',
title: 'Chat',
group: 'general',
kind: 'single',
keywords: ['tools', 'diff', 'reasoning', 'dotfiles', 'draft', 'queue', 'output'],
},
{
slug: 'shortcuts',
title: 'Shortcuts',
group: 'general',
kind: 'single',
keywords: ['keyboard', 'hotkeys', 'shortcuts', 'bindings'],
},
{
slug: 'sessions',
title: 'Sessions',
group: 'general',
kind: 'single',
keywords: ['defaults', 'default agent', 'default model', 'retention', 'memory', 'limits', 'zen'],
},
{ slug: 'notifications', title: 'Notifications', group: 'general', kind: 'single', keywords: ['alerts', 'native', 'summary', 'summarization'], },
{ slug: 'voice', title: 'Voice', group: 'advanced', kind: 'single', keywords: ['tts', 'speech', 'voice'], isAvailable: (ctx) => !ctx.isVSCode },
] as const;
export const LEGACY_SIDEBAR_SECTION_TO_SETTINGS_SLUG: Record<SidebarSection, SettingsPageSlug> = {
sessions: 'sessions',
agents: 'agents',
commands: 'commands',
mcp: 'mcp',
skills: 'skills.installed',
providers: 'providers',
usage: 'usage',
'git-identities': 'git',
settings: 'home',
};
export function getSettingsPageMeta(slug: string): SettingsPageMeta | null {
const normalized = slug.trim().toLowerCase();
return (SETTINGS_PAGE_METADATA as readonly SettingsPageMeta[]).find((page) => page.slug === normalized) ?? null;
}
export function resolveSettingsSlug(value: string | null | undefined): SettingsPageSlug {
const normalized = (value ?? '').trim().toLowerCase();
if (!normalized) {
return 'home';
}
const legacy = (LEGACY_SIDEBAR_SECTION_TO_SETTINGS_SLUG as Record<string, SettingsPageSlug>)[normalized];
if (legacy) {
return legacy;
}
const direct = getSettingsPageMeta(normalized);
if (direct) {
return direct.slug;
}
return 'home';
}
@@ -9,15 +9,15 @@
},
"colors": {
"primary": {
"base": "#EC8B49",
"base": "#DA702C",
"hover": "#DA702C",
"active": "#F9AE77",
"foreground": "#100F0F",
"muted": "#EC8B4980",
"foreground": "#111010",
"muted": "#DA702C80",
"emphasis": "#F9AE77"
},
"surface": {
"background": "#100F0F",
"background": "#111010",
"foreground": "#CECDC3",
"muted": "#1C1B1A",
"mutedForeground": "#878580",
@@ -29,30 +29,30 @@
"interactive": {
"border": "#343331",
"borderHover": "#403E3C",
"borderFocus": "#EC8B49",
"borderFocus": "#DA702C",
"selection": "#f4f4f41f",
"selectionForeground": "#CECDC3",
"focus": "#EC8B49",
"focusRing": "#EC8B4950",
"focus": "#DA702C",
"focusRing": "#DA702C50",
"cursor": "#CECDC3",
"hover": "#ffffff18",
"active": "#ffffff1f"
},
"status": {
"error": "#D14D41",
"errorForeground": "#100F0F",
"errorForeground": "#111010",
"errorBackground": "#AF302920",
"errorBorder": "#AF302950",
"warning": "#DA702C",
"warningForeground": "#100F0F",
"warningForeground": "#111010",
"warningBackground": "#BC521520",
"warningBorder": "#BC521550",
"success": "#A0AF54",
"successForeground": "#100F0F",
"successForeground": "#111010",
"successBackground": "#66800B20",
"successBorder": "#66800B50",
"info": "#4385BE",
"infoForeground": "#100F0F",
"infoForeground": "#111010",
"infoBackground": "#205EA620",
"infoBorder": "#205EA650"
},
@@ -138,9 +138,9 @@
},
"chat": {
"userMessage": "#CECDC3",
"userMessageBackground": "#2d1d15",
"userMessageBackground": "#2E1B10",
"assistantMessage": "#CECDC3",
"assistantMessageBackground": "#100F0F",
"assistantMessageBackground": "#111010",
"timestamp": "#878580",
"divider": "#343331"
},
@@ -9,11 +9,11 @@
},
"colors": {
"primary": {
"base": "#EC8B49",
"base": "#BC5215",
"hover": "#F9AE77",
"active": "#DA702C",
"foreground": "#FFFCF0",
"muted": "#EC8B4980",
"muted": "#BC521580",
"emphasis": "#F9AE77"
},
"surface": {
@@ -24,16 +24,16 @@
"elevated": "#fdf6ec",
"elevatedForeground": "#100F0F",
"overlay": "#100F0F20",
"subtle": "#f9f1e4"
"subtle": "#F6F0E6"
},
"interactive": {
"border": "#DAD8CE",
"borderHover": "#CECDC3",
"borderFocus": "#EC8B49",
"borderFocus": "#BC5215",
"selection": "#76736f30",
"selectionForeground": "#100F0F",
"focus": "#EC8B49",
"focusRing": "#EC8B4940",
"focus": "#BC5215",
"focusRing": "#BC521540",
"cursor": "#100F0F",
"hover": "#76736f20",
"active": "#76736f20"
@@ -138,7 +138,7 @@
},
"chat": {
"userMessage": "#100F0F",
"userMessageBackground": "#f9f1e4",
"userMessageBackground": "#F6F0E6",
"assistantMessage": "#100F0F",
"assistantMessageBackground": "#FFFCF0",
"timestamp": "#6F6E69",