feat: add UI customization and model management features (#60)
* feat: add UI customization and model management features Add comprehensive UI customization options and enhanced model selection: - **Favorite & Recent Models**: Star models for quick access, track 5 most recent - Add favorite/recent sections to model dropdowns - Persist preferences in local storage - Works in ModelControls and ModelSelector components - **Tool Call Expansion Settings**: Control default expansion state for tool outputs - Three modes: collapsed, activity (summary), detailed - Applies to activity groups and individual tool calls - Configurable in Appearance Settings - **Font Size & Spacing Controls**: Adjustable typography and layout density - Font size: 50-200% scaling of all semantic typography - Spacing/padding: 50-200% scaling of margins, gaps, line heights - Real-time preview with reset buttons - Settings in Appearance Settings - **VSCode Extension Settings View**: Full settings access in VSCode extension - Add settings navigation and view type - Settings button in header - Navigate between sessions, chat, and settings Technical changes: - Enhanced useUIStore with new state management - Dynamic CSS variable scaling for typography and spacing - Typography helper functions for variable access - ThemeProvider integration for auto-applying scales * fix: hide theme mode in VSCode and fix detailed tool expansion - Hide "Theme Mode" setting in VSCode extension settings as VSCode dynamically applies its own theme to the extension - Fix bug where tools from subsequent messages in a turn weren't expanded when "Detailed" mode was selected - Now correctly aggregates all tool IDs from turnGroupingContext when calculating effective expanded tools for progressive groups
This commit is contained in:
@@ -2,6 +2,7 @@ import { create } from 'zustand';
|
||||
import { devtools, persist, createJSONStorage } from 'zustand/middleware';
|
||||
import type { SidebarSection } from '@/constants/sidebar';
|
||||
import { getSafeStorage } from './utils/safeStorage';
|
||||
import { getTypographyVariable, type SemanticTypographyKey } from '@/lib/typography';
|
||||
|
||||
export type MainTab = 'chat' | 'git' | 'diff' | 'terminal';
|
||||
export type EventStreamStatus =
|
||||
@@ -32,6 +33,13 @@ interface UIStore {
|
||||
eventStreamHint: string | null;
|
||||
showReasoningTraces: boolean;
|
||||
|
||||
toolCallExpansion: 'collapsed' | 'activity' | 'detailed';
|
||||
fontSize: number;
|
||||
padding: number;
|
||||
|
||||
favoriteModels: Array<{ providerID: string; modelID: string }>;
|
||||
recentModels: Array<{ providerID: string; modelID: string }>;
|
||||
|
||||
diffLayoutPreference: 'dynamic' | 'inline' | 'side-by-side';
|
||||
diffFileLayout: Record<string, 'inline' | 'side-by-side'>;
|
||||
diffWrapLines: boolean;
|
||||
@@ -56,7 +64,15 @@ interface UIStore {
|
||||
setSidebarSection: (section: SidebarSection) => void;
|
||||
setEventStreamStatus: (status: EventStreamStatus, hint?: string | null) => void;
|
||||
setShowReasoningTraces: (value: boolean) => void;
|
||||
setToolCallExpansion: (value: 'collapsed' | 'activity' | 'detailed') => void;
|
||||
setFontSize: (size: number) => void;
|
||||
setPadding: (size: number) => void;
|
||||
applyTypography: () => void;
|
||||
applyPadding: () => void;
|
||||
updateProportionalSidebarWidths: () => void;
|
||||
toggleFavoriteModel: (providerID: string, modelID: string) => void;
|
||||
isFavoriteModel: (providerID: string, modelID: string) => boolean;
|
||||
addRecentModel: (providerID: string, modelID: string) => void;
|
||||
setDiffLayoutPreference: (mode: 'dynamic' | 'inline' | 'side-by-side') => void;
|
||||
setDiffFileLayout: (filePath: string, mode: 'inline' | 'side-by-side') => void;
|
||||
setDiffWrapLines: (wrap: boolean) => void;
|
||||
@@ -83,6 +99,11 @@ export const useUIStore = create<UIStore>()(
|
||||
eventStreamStatus: 'idle',
|
||||
eventStreamHint: null,
|
||||
showReasoningTraces: false,
|
||||
toolCallExpansion: 'collapsed',
|
||||
fontSize: 100,
|
||||
padding: 100,
|
||||
favoriteModels: [],
|
||||
recentModels: [],
|
||||
diffLayoutPreference: 'dynamic',
|
||||
diffFileLayout: {},
|
||||
diffWrapLines: false,
|
||||
@@ -194,6 +215,78 @@ export const useUIStore = create<UIStore>()(
|
||||
set({ showReasoningTraces: value });
|
||||
},
|
||||
|
||||
setToolCallExpansion: (value) => {
|
||||
set({ toolCallExpansion: value });
|
||||
},
|
||||
|
||||
setFontSize: (size) => {
|
||||
// Clamp between 50% and 200%
|
||||
const clampedSize = Math.max(50, Math.min(200, size));
|
||||
set({ fontSize: clampedSize });
|
||||
get().applyTypography();
|
||||
},
|
||||
|
||||
setPadding: (size) => {
|
||||
// Clamp between 50% and 200%
|
||||
const clampedSize = Math.max(50, Math.min(200, size));
|
||||
set({ padding: clampedSize });
|
||||
get().applyPadding();
|
||||
},
|
||||
|
||||
applyTypography: () => {
|
||||
const { fontSize } = get();
|
||||
const root = document.documentElement;
|
||||
|
||||
// Apply font size as a percentage scale
|
||||
// 100 = default (1.0x), 50 = half size (0.5x), 200 = double (2.0x)
|
||||
const scale = fontSize / 100;
|
||||
|
||||
// Store scale for reference
|
||||
root.style.setProperty('--font-scale', scale.toString());
|
||||
|
||||
// Read base values from SEMANTIC_TYPOGRAPHY or use defaults
|
||||
const baseValues: Record<string, string> = {
|
||||
markdown: '0.9375rem',
|
||||
code: '0.9063rem',
|
||||
uiHeader: '0.9375rem',
|
||||
uiLabel: '0.875rem',
|
||||
meta: '0.875rem',
|
||||
micro: '0.875rem',
|
||||
};
|
||||
|
||||
// Apply scaled values to each typography variable
|
||||
Object.entries(baseValues).forEach(([key, baseValue]) => {
|
||||
const cssVar = getTypographyVariable(key as SemanticTypographyKey);
|
||||
const numericValue = parseFloat(baseValue);
|
||||
if (!isNaN(numericValue)) {
|
||||
root.style.setProperty(cssVar, `${numericValue * scale}rem`);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
applyPadding: () => {
|
||||
const { padding } = get();
|
||||
const root = document.documentElement;
|
||||
|
||||
// Apply padding as a percentage scale with non-linear scaling
|
||||
// Use square root for more natural scaling at extremes
|
||||
const scale = padding / 100;
|
||||
const adjustedScale = Math.sqrt(scale);
|
||||
|
||||
// Set the CSS custom property that all spacing tokens reference
|
||||
root.style.setProperty('--padding-scale', adjustedScale.toString());
|
||||
|
||||
// Apply line height scaling - use much smaller scale factor
|
||||
// Line height should remain relatively constant even when font size changes
|
||||
// Use a dampened scale: 50% font = 0.9x line-height, 200% font = 1.1x line-height
|
||||
const lineHeightScale = 1 + (scale - 1) * 0.15; // Reduces impact: 50% -> 0.925, 200% -> 1.15
|
||||
|
||||
root.style.setProperty('--line-height-tight', (1.25 * lineHeightScale).toFixed(3));
|
||||
root.style.setProperty('--line-height-normal', (1.5 * lineHeightScale).toFixed(3));
|
||||
root.style.setProperty('--line-height-relaxed', (1.625 * lineHeightScale).toFixed(3));
|
||||
root.style.setProperty('--line-height-loose', (2 * lineHeightScale).toFixed(3));
|
||||
},
|
||||
|
||||
setDiffLayoutPreference: (mode) => {
|
||||
set({ diffLayoutPreference: mode });
|
||||
},
|
||||
@@ -211,6 +304,48 @@ export const useUIStore = create<UIStore>()(
|
||||
set({ diffWrapLines: wrap });
|
||||
},
|
||||
|
||||
toggleFavoriteModel: (providerID, modelID) => {
|
||||
set((state) => {
|
||||
const exists = state.favoriteModels.some(
|
||||
(fav) => fav.providerID === providerID && fav.modelID === modelID
|
||||
);
|
||||
|
||||
if (exists) {
|
||||
// Remove from favorites
|
||||
return {
|
||||
favoriteModels: state.favoriteModels.filter(
|
||||
(fav) => !(fav.providerID === providerID && fav.modelID === modelID)
|
||||
),
|
||||
};
|
||||
} else {
|
||||
// Add to favorites (newest first)
|
||||
return {
|
||||
favoriteModels: [{ providerID, modelID }, ...state.favoriteModels],
|
||||
};
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
isFavoriteModel: (providerID, modelID) => {
|
||||
const { favoriteModels } = get();
|
||||
return favoriteModels.some(
|
||||
(fav) => fav.providerID === providerID && fav.modelID === modelID
|
||||
);
|
||||
},
|
||||
|
||||
addRecentModel: (providerID, modelID) => {
|
||||
set((state) => {
|
||||
// Remove existing instance if any
|
||||
const filtered = state.recentModels.filter(
|
||||
(m) => !(m.providerID === providerID && m.modelID === modelID)
|
||||
);
|
||||
// Add to front, limit to 5
|
||||
return {
|
||||
recentModels: [{ providerID, modelID }, ...filtered].slice(0, 5),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
updateProportionalSidebarWidths: () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
@@ -254,6 +389,11 @@ export const useUIStore = create<UIStore>()(
|
||||
isSessionCreateDialogOpen: state.isSessionCreateDialogOpen,
|
||||
isSettingsDialogOpen: state.isSettingsDialogOpen,
|
||||
showReasoningTraces: state.showReasoningTraces,
|
||||
toolCallExpansion: state.toolCallExpansion,
|
||||
fontSize: state.fontSize,
|
||||
padding: state.padding,
|
||||
favoriteModels: state.favoriteModels,
|
||||
recentModels: state.recentModels,
|
||||
diffLayoutPreference: state.diffLayoutPreference,
|
||||
diffWrapLines: state.diffWrapLines,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user