import { writable, derived } from "svelte/store"; import { invoke } from "@tauri-apps/api/core"; export type Theme = "dark" | "light" | "high-contrast" | "custom"; export type BudgetAction = "warn" | "block"; export interface CustomThemeColors { bg_primary: string | null; bg_secondary: string | null; bg_terminal: string | null; accent_primary: string | null; accent_secondary: string | null; text_primary: string | null; text_secondary: string | null; border_color: string | null; } export interface HikariConfig { model: string | null; api_key: string | null; custom_instructions: string | null; mcp_servers_json: string | null; auto_granted_tools: string[]; theme: Theme; greeting_enabled: boolean; greeting_custom_prompt: string | null; notifications_enabled: boolean; notification_volume: number; always_on_top: boolean; update_checks_enabled: boolean; character_panel_width: number | null; font_size: number; streamer_mode: boolean; streamer_hide_paths: boolean; compact_mode: boolean; profile_name: string | null; profile_avatar_path: string | null; profile_bio: string | null; custom_theme_colors: CustomThemeColors; // Budget settings budget_enabled: boolean; session_token_budget: number | null; session_cost_budget: number | null; budget_action: BudgetAction; budget_warning_threshold: number; // Discord RPC settings discord_rpc_enabled: boolean; // Thinking blocks settings show_thinking_blocks: boolean; // Worktree isolation use_worktree: boolean; } const defaultConfig: HikariConfig = { model: null, api_key: null, custom_instructions: null, mcp_servers_json: null, auto_granted_tools: [], theme: "dark", greeting_enabled: true, greeting_custom_prompt: null, notifications_enabled: true, notification_volume: 0.7, always_on_top: false, update_checks_enabled: true, character_panel_width: null, font_size: 14, streamer_mode: false, streamer_hide_paths: false, compact_mode: false, profile_name: null, profile_avatar_path: null, profile_bio: null, custom_theme_colors: { bg_primary: null, bg_secondary: null, bg_terminal: null, accent_primary: null, accent_secondary: null, text_primary: null, text_secondary: null, border_color: null, }, budget_enabled: false, session_token_budget: null, session_cost_budget: null, budget_action: "warn", budget_warning_threshold: 0.8, discord_rpc_enabled: true, show_thinking_blocks: true, use_worktree: false, }; function createConfigStore() { const config = writable(defaultConfig); const isLoading = writable(true); const isSidebarOpen = writable(false); const saveError = writable(null); // Internal function to get current config synchronously function getCurrentConfig(): HikariConfig { let currentConfig: HikariConfig = defaultConfig; const unsubscribe = config.subscribe((c) => (currentConfig = c)); unsubscribe(); return currentConfig; } async function loadConfig() { isLoading.set(true); try { const savedConfig = await invoke("get_config"); config.set(savedConfig); } catch (error) { console.error("Failed to load config:", error); config.set(defaultConfig); } finally { isLoading.set(false); } } async function saveConfig(newConfig: HikariConfig) { saveError.set(null); try { await invoke("save_config", { config: newConfig }); config.set(newConfig); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); saveError.set(errorMessage); console.error("Failed to save config:", error); throw error; } } async function updateConfig(updates: Partial) { const currentConfig = getCurrentConfig(); const newConfig = { ...currentConfig, ...updates }; await saveConfig(newConfig); } return { config: { subscribe: config.subscribe }, isLoading: { subscribe: isLoading.subscribe }, isSidebarOpen: { subscribe: isSidebarOpen.subscribe }, saveError: { subscribe: saveError.subscribe }, loadConfig, saveConfig, updateConfig, openSidebar: () => isSidebarOpen.set(true), closeSidebar: () => isSidebarOpen.set(false), toggleSidebar: () => isSidebarOpen.update((open) => !open), setTheme: async (theme: Theme, customColors?: CustomThemeColors) => { const updates: Partial = { theme }; if (customColors) { updates.custom_theme_colors = customColors; } await updateConfig(updates); const currentConfig = getCurrentConfig(); applyTheme(theme, currentConfig.custom_theme_colors); }, setCustomThemeColors: async (colors: CustomThemeColors) => { await updateConfig({ custom_theme_colors: colors }); const currentConfig = getCurrentConfig(); if (currentConfig.theme === "custom") { applyCustomThemeColors(colors); } }, setFontSize: async (size: number) => { const clampedSize = Math.max(MIN_FONT_SIZE, Math.min(MAX_FONT_SIZE, size)); await updateConfig({ font_size: clampedSize }); applyFontSize(clampedSize); }, increaseFontSize: async () => { const currentConfig = getCurrentConfig(); const newSize = Math.min(MAX_FONT_SIZE, currentConfig.font_size + 2); await updateConfig({ font_size: newSize }); applyFontSize(newSize); }, decreaseFontSize: async () => { const currentConfig = getCurrentConfig(); const newSize = Math.max(MIN_FONT_SIZE, currentConfig.font_size - 2); await updateConfig({ font_size: newSize }); applyFontSize(newSize); }, resetFontSize: async () => { await updateConfig({ font_size: DEFAULT_FONT_SIZE }); applyFontSize(DEFAULT_FONT_SIZE); }, addAutoGrantedTool: async (tool: string) => { const currentConfig = getCurrentConfig(); if (!currentConfig.auto_granted_tools.includes(tool)) { const newTools = [...currentConfig.auto_granted_tools, tool]; await updateConfig({ auto_granted_tools: newTools }); } }, removeAutoGrantedTool: async (tool: string) => { const currentConfig = getCurrentConfig(); const newTools = currentConfig.auto_granted_tools.filter((t) => t !== tool); await updateConfig({ auto_granted_tools: newTools }); }, getConfig: (): HikariConfig => { return getCurrentConfig(); }, toggleStreamerMode: async () => { const currentConfig = getCurrentConfig(); await updateConfig({ streamer_mode: !currentConfig.streamer_mode }); }, toggleCompactMode: async () => { const currentConfig = getCurrentConfig(); await updateConfig({ compact_mode: !currentConfig.compact_mode }); }, setCompactMode: async (enabled: boolean) => { await updateConfig({ compact_mode: enabled }); }, }; } export function applyTheme(theme: Theme, customColors?: CustomThemeColors) { if (typeof document !== "undefined") { // For custom theme, we use dark as the base and override with custom colors document.documentElement.setAttribute("data-theme", theme === "custom" ? "dark" : theme); // Clear any previously applied custom colors clearCustomThemeColors(); // Apply custom colors if theme is custom if (theme === "custom" && customColors) { applyCustomThemeColors(customColors); } } } export function applyCustomThemeColors(colors: CustomThemeColors) { if (typeof document === "undefined") return; const root = document.documentElement; if (colors.bg_primary) root.style.setProperty("--bg-primary", colors.bg_primary); if (colors.bg_secondary) root.style.setProperty("--bg-secondary", colors.bg_secondary); if (colors.bg_terminal) root.style.setProperty("--bg-terminal", colors.bg_terminal); if (colors.accent_primary) root.style.setProperty("--accent-primary", colors.accent_primary); if (colors.accent_secondary) root.style.setProperty("--accent-secondary", colors.accent_secondary); if (colors.text_primary) root.style.setProperty("--text-primary", colors.text_primary); if (colors.text_secondary) root.style.setProperty("--text-secondary", colors.text_secondary); if (colors.border_color) root.style.setProperty("--border-color", colors.border_color); } export function clearCustomThemeColors() { if (typeof document === "undefined") return; const root = document.documentElement; const customProperties = [ "--bg-primary", "--bg-secondary", "--bg-terminal", "--accent-primary", "--accent-secondary", "--text-primary", "--text-secondary", "--border-color", ]; customProperties.forEach((prop) => root.style.removeProperty(prop)); } const MIN_FONT_SIZE = 10; const MAX_FONT_SIZE = 24; const DEFAULT_FONT_SIZE = 14; export function applyFontSize(size: number) { if (typeof document !== "undefined") { const clampedSize = Math.max(MIN_FONT_SIZE, Math.min(MAX_FONT_SIZE, size)); document.documentElement.style.setProperty("--terminal-font-size", `${clampedSize}px`); } } export function clampFontSize(size: number): number { return Math.max(MIN_FONT_SIZE, Math.min(MAX_FONT_SIZE, size)); } export { MIN_FONT_SIZE, MAX_FONT_SIZE, DEFAULT_FONT_SIZE }; export const configStore = createConfigStore(); export const isDarkTheme = derived(configStore.config, ($config) => $config.theme === "dark"); export const isStreamerMode = derived(configStore.config, ($config) => $config.streamer_mode); export const isCompactMode = derived(configStore.config, ($config) => $config.compact_mode); export const shouldHidePaths = derived( configStore.config, ($config) => $config.streamer_mode && $config.streamer_hide_paths ); export const showThinkingBlocks = derived( configStore.config, ($config) => $config.show_thinking_blocks ); /** * Masks file paths in text when streamer mode with hide paths is enabled. * Replaces username portion of paths with asterisks. */ export function maskPaths(text: string, hidePaths: boolean): string { if (!hidePaths) return text; // Match Unix paths like /home/username/... or /Users/username/... // and Windows paths like C:\Users\username\... return text .replace(/\/home\/([^/\s]+)/g, "/home/****") .replace(/\/Users\/([^/\s]+)/g, "/Users/****") .replace(/C:\\Users\\([^\\\s]+)/gi, "C:\\Users\\****") .replace(/~\//g, "****/"); }