From 5209395a7477cb3dcd96cc0e2b2f99e01a964dc9 Mon Sep 17 00:00:00 2001 From: Stefan de Vogelaere Date: Sat, 17 Jan 2026 11:44:33 +0100 Subject: [PATCH 1/4] fix: respect theme in agent output modal and log viewer The Agent Output modal and LogViewer component had hardcoded dark zinc colors that didn't adapt to light mode themes. Replaced all hardcoded colors with semantic Tailwind classes (bg-popover, text-foreground, text-muted-foreground, bg-muted, border-border) that automatically respect the active theme. --- apps/ui/src/components/ui/log-viewer.tsx | 52 ++++++++++--------- .../board-view/dialogs/agent-output-modal.tsx | 6 ++- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/apps/ui/src/components/ui/log-viewer.tsx b/apps/ui/src/components/ui/log-viewer.tsx index 5284ef2d..1d14a14e 100644 --- a/apps/ui/src/components/ui/log-viewer.tsx +++ b/apps/ui/src/components/ui/log-viewer.tsx @@ -108,7 +108,7 @@ const getToolCategoryColor = (category: ToolCategory | undefined): string => { case 'task': return 'text-indigo-400 bg-indigo-500/10 border-indigo-500/30'; default: - return 'text-zinc-400 bg-zinc-500/10 border-zinc-500/30'; + return 'text-muted-foreground bg-muted/30 border-border'; } }; @@ -150,9 +150,9 @@ function TodoListRenderer({ todos }: { todos: TodoItem[] }) { case 'in_progress': return ; case 'pending': - return ; + return ; default: - return ; + return ; } }; @@ -163,9 +163,9 @@ function TodoListRenderer({ todos }: { todos: TodoItem[] }) { case 'in_progress': return 'text-amber-300'; case 'pending': - return 'text-zinc-400'; + return 'text-muted-foreground'; default: - return 'text-zinc-400'; + return 'text-muted-foreground'; } }; @@ -197,7 +197,7 @@ function TodoListRenderer({ todos }: { todos: TodoItem[] }) { 'flex items-start gap-2 p-2 rounded-md transition-colors', todo.status === 'in_progress' && 'bg-amber-500/5 border border-amber-500/20', todo.status === 'completed' && 'bg-emerald-500/5', - todo.status === 'pending' && 'bg-zinc-800/30' + todo.status === 'pending' && 'bg-muted/30' )} >
{getStatusIcon(todo.status)}
@@ -313,9 +313,9 @@ function LogEntryItem({ entry, isExpanded, onToggle }: LogEntryItemProps) { // Get colors - use tool category colors for tool_call entries const colorParts = toolCategoryColors.split(' '); - const textColor = isToolCall ? colorParts[0] || 'text-zinc-400' : colors.text; - const bgColor = isToolCall ? colorParts[1] || 'bg-zinc-500/10' : colors.bg; - const borderColor = isToolCall ? colorParts[2] || 'border-zinc-500/30' : colors.border; + const textColor = isToolCall ? colorParts[0] || 'text-muted-foreground' : colors.text; + const bgColor = isToolCall ? colorParts[1] || 'bg-muted/30' : colors.bg; + const borderColor = isToolCall ? colorParts[2] || 'border-border' : colors.border; return (
{hasContent ? ( isExpanded ? ( - + ) : ( - + ) ) : ( @@ -361,7 +361,9 @@ function LogEntryItem({ entry, isExpanded, onToggle }: LogEntryItemProps) { {entry.title} - {collapsedPreview} + + {collapsedPreview} + {(isExpanded || !hasContent) && ( @@ -374,7 +376,7 @@ function LogEntryItem({ entry, isExpanded, onToggle }: LogEntryItemProps) { {formattedContent.map((part, index) => (
{part.type === 'json' ? ( -
+                    
                       {part.content}
                     
) : ( @@ -576,7 +578,7 @@ export function LogViewer({ output, className }: LogViewerProps) {

No log entries yet. Logs will appear here as the process runs.

{output && output.trim() && ( -
+
{output}
)} @@ -610,23 +612,23 @@ export function LogViewer({ output, className }: LogViewerProps) {
{/* Sticky header with search, stats, and filters */} {/* Use -top-4 to compensate for parent's p-4 padding, pt-4 to restore visual spacing */} -
+
{/* Search bar */}
- + setSearchQuery(e.target.value)} placeholder="Search logs..." - className="w-full pl-8 pr-8 py-1.5 text-xs bg-zinc-900/50 border border-zinc-700/50 rounded-md text-zinc-200 placeholder:text-zinc-500 focus:outline-none focus:border-zinc-600" + className="w-full pl-8 pr-8 py-1.5 text-xs bg-muted/50 border border-border rounded-md text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-ring" data-testid="log-search-input" /> {searchQuery && (
@@ -358,25 +340,13 @@ export function ProjectThemeSection({ project }: ProjectThemeSectionProps) { - +
)}
diff --git a/apps/ui/src/components/views/settings-view/appearance/appearance-section.tsx b/apps/ui/src/components/views/settings-view/appearance/appearance-section.tsx index 400d121c..f449140b 100644 --- a/apps/ui/src/components/views/settings-view/appearance/appearance-section.tsx +++ b/apps/ui/src/components/views/settings-view/appearance/appearance-section.tsx @@ -1,12 +1,5 @@ import { useState, useEffect } from 'react'; import { Label } from '@/components/ui/label'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; import { Palette, Moon, Sun, Type } from 'lucide-react'; import { darkThemes, lightThemes } from '@/config/theme-options'; import { @@ -16,6 +9,7 @@ import { } from '@/config/ui-font-options'; import { cn } from '@/lib/utils'; import { useAppStore } from '@/store/app-store'; +import { FontSelector } from '@/components/shared'; import type { Theme } from '../shared/types'; interface AppearanceSectionProps { @@ -165,25 +159,13 @@ export function AppearanceSection({ effectiveTheme, onThemeChange }: AppearanceS - +

Used for headings, labels, and UI text

@@ -194,25 +176,13 @@ export function AppearanceSection({ effectiveTheme, onThemeChange }: AppearanceS - +

Used for code blocks and monospaced text

diff --git a/apps/ui/src/hooks/use-settings-sync.ts b/apps/ui/src/hooks/use-settings-sync.ts index c9430684..ea865566 100644 --- a/apps/ui/src/hooks/use-settings-sync.ts +++ b/apps/ui/src/hooks/use-settings-sync.ts @@ -79,6 +79,41 @@ const SETTINGS_FIELDS_TO_SYNC = [ // Fields from setup store to sync const SETUP_FIELDS_TO_SYNC = ['isFirstRun', 'setupComplete', 'skipClaudeSetup'] as const; +/** + * Helper to extract a settings field value from app state + * Handles special cases for nested/mapped fields + */ +function getSettingsFieldValue( + field: (typeof SETTINGS_FIELDS_TO_SYNC)[number], + appState: ReturnType +): unknown { + if (field === 'currentProjectId') { + return appState.currentProject?.id ?? null; + } + if (field === 'terminalFontFamily') { + return appState.terminalState.fontFamily; + } + return appState[field as keyof typeof appState]; +} + +/** + * Helper to check if a settings field changed between states + */ +function hasSettingsFieldChanged( + field: (typeof SETTINGS_FIELDS_TO_SYNC)[number], + newState: ReturnType, + prevState: ReturnType +): boolean { + if (field === 'currentProjectId') { + return newState.currentProject?.id !== prevState.currentProject?.id; + } + if (field === 'terminalFontFamily') { + return newState.terminalState.fontFamily !== prevState.terminalState.fontFamily; + } + const key = field as keyof typeof newState; + return newState[key] !== prevState[key]; +} + interface SettingsSyncState { /** Whether initial settings have been loaded from API */ loaded: boolean; @@ -157,15 +192,7 @@ export function useSettingsSync(): SettingsSyncState { // Build updates object from current state const updates: Record = {}; for (const field of SETTINGS_FIELDS_TO_SYNC) { - if (field === 'currentProjectId') { - // Special handling: extract ID from currentProject object - updates[field] = appState.currentProject?.id ?? null; - } else if (field === 'terminalFontFamily') { - // Special handling: map terminalState.fontFamily to terminalFontFamily - updates[field] = appState.terminalState.fontFamily; - } else { - updates[field] = appState[field as keyof typeof appState]; - } + updates[field] = getSettingsFieldValue(field, appState); } // Include setup wizard state (lives in a separate store) @@ -262,13 +289,7 @@ export function useSettingsSync(): SettingsSyncState { // (migration has already hydrated the store from server/localStorage) const updates: Record = {}; for (const field of SETTINGS_FIELDS_TO_SYNC) { - if (field === 'currentProjectId') { - updates[field] = appState.currentProject?.id ?? null; - } else if (field === 'terminalFontFamily') { - updates[field] = appState.terminalState.fontFamily; - } else { - updates[field] = appState[field as keyof typeof appState]; - } + updates[field] = getSettingsFieldValue(field, appState); } for (const field of SETUP_FIELDS_TO_SYNC) { updates[field] = setupState[field as keyof typeof setupState]; @@ -322,24 +343,9 @@ export function useSettingsSync(): SettingsSyncState { // Check if any synced field changed let changed = false; for (const field of SETTINGS_FIELDS_TO_SYNC) { - if (field === 'currentProjectId') { - // Special handling: compare currentProject IDs - if (newState.currentProject?.id !== prevState.currentProject?.id) { - changed = true; - break; - } - } else if (field === 'terminalFontFamily') { - // Special handling: compare terminalState.fontFamily - if (newState.terminalState.fontFamily !== prevState.terminalState.fontFamily) { - changed = true; - break; - } - } else { - const key = field as keyof typeof newState; - if (newState[key] !== prevState[key]) { - changed = true; - break; - } + if (hasSettingsFieldChanged(field, newState, prevState)) { + changed = true; + break; } } @@ -413,13 +419,7 @@ export async function forceSyncSettingsToServer(): Promise { const updates: Record = {}; for (const field of SETTINGS_FIELDS_TO_SYNC) { - if (field === 'currentProjectId') { - updates[field] = appState.currentProject?.id ?? null; - } else if (field === 'terminalFontFamily') { - updates[field] = appState.terminalState.fontFamily; - } else { - updates[field] = appState[field as keyof typeof appState]; - } + updates[field] = getSettingsFieldValue(field, appState); } const setupState = useSetupStore.getState(); for (const field of SETUP_FIELDS_TO_SYNC) { diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index 121fb8cd..75c97ccb 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -149,6 +149,31 @@ export function getStoredTheme(): ThemeMode | null { return null; } +/** + * Helper to get effective font value with validation + * Returns the font to use (project override -> global -> null for default) + * @param projectFont - The project-specific font override + * @param globalFont - The global font setting + * @param fontOptions - The list of valid font options for validation + */ +function getEffectiveFont( + projectFont: string | undefined, + globalFont: string | null, + fontOptions: readonly { value: string; label: string }[] +): string | null { + const isValidFont = (font: string | null | undefined): boolean => { + if (!font || font === DEFAULT_FONT_VALUE) return true; + return fontOptions.some((opt) => opt.value === font); + }; + + if (projectFont) { + if (!isValidFont(projectFont)) return null; // Fallback to default if font not in list + return projectFont === DEFAULT_FONT_VALUE ? null : projectFont; + } + if (!isValidFont(globalFont)) return null; // Fallback to default if font not in list + return globalFont === DEFAULT_FONT_VALUE ? null : globalFont; +} + /** * Save theme to localStorage for immediate persistence * This is used as a fallback when server settings can't be loaded @@ -1873,43 +1898,13 @@ export const useAppStore = create()((set, get) => ({ }, getEffectiveFontSans: () => { - const currentProject = get().currentProject; - // Return project override if set, otherwise global, otherwise null for default - // 'default' value means explicitly using default font, so return null for CSS - // Also validate that the font is in the available options list - const isValidFont = (font: string | null | undefined): boolean => { - if (!font || font === DEFAULT_FONT_VALUE) return true; - return UI_SANS_FONT_OPTIONS.some((opt) => opt.value === font); - }; - - if (currentProject?.fontFamilySans) { - const font = currentProject.fontFamilySans; - if (!isValidFont(font)) return null; // Fallback to default if font not in list - return font === DEFAULT_FONT_VALUE ? null : font; - } - const globalFont = get().fontFamilySans; - if (!isValidFont(globalFont)) return null; // Fallback to default if font not in list - return globalFont === DEFAULT_FONT_VALUE ? null : globalFont; + const { currentProject, fontFamilySans } = get(); + return getEffectiveFont(currentProject?.fontFamilySans, fontFamilySans, UI_SANS_FONT_OPTIONS); }, getEffectiveFontMono: () => { - const currentProject = get().currentProject; - // Return project override if set, otherwise global, otherwise null for default - // 'default' value means explicitly using default font, so return null for CSS - // Also validate that the font is in the available options list - const isValidFont = (font: string | null | undefined): boolean => { - if (!font || font === DEFAULT_FONT_VALUE) return true; - return UI_MONO_FONT_OPTIONS.some((opt) => opt.value === font); - }; - - if (currentProject?.fontFamilyMono) { - const font = currentProject.fontFamilyMono; - if (!isValidFont(font)) return null; // Fallback to default if font not in list - return font === DEFAULT_FONT_VALUE ? null : font; - } - const globalFont = get().fontFamilyMono; - if (!isValidFont(globalFont)) return null; // Fallback to default if font not in list - return globalFont === DEFAULT_FONT_VALUE ? null : globalFont; + const { currentProject, fontFamilyMono } = get(); + return getEffectiveFont(currentProject?.fontFamilyMono, fontFamilyMono, UI_MONO_FONT_OPTIONS); }, // Feature actions From aef479218dfa467bf4ff930d581d0c0714d1a80f Mon Sep 17 00:00:00 2001 From: Stefan de Vogelaere Date: Sat, 17 Jan 2026 19:32:42 +0100 Subject: [PATCH 4/4] fix: use DEFAULT_FONT_VALUE for initial terminal font The initial terminalState.fontFamily was set to a raw font string that didn't match any option in TERMINAL_FONT_OPTIONS, causing the dropdown to appear empty. Changed to use DEFAULT_FONT_VALUE sentinel. --- apps/ui/src/store/app-store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index 75c97ccb..a23c17c4 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -1440,7 +1440,7 @@ const initialState: AppState = { defaultFontSize: 14, defaultRunScript: '', screenReaderMode: false, - fontFamily: "Menlo, Monaco, 'Courier New', monospace", + fontFamily: DEFAULT_FONT_VALUE, scrollbackLines: 5000, lineHeight: 1.0, maxSessions: 100,