From efe7af6f77f870078ff751137860a690dcb19235 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Fri, 14 Nov 2025 20:42:13 +0000 Subject: [PATCH] Introduce ConfigProvider to stabilize preference saves - move config state into a dedicated context provider that eagerly hydrates disk state before any write - update App, folder selection, message rendering, and advanced settings to consume the context instead of globals - wrap the renderer entry in ConfigProvider so every view shares the same initialized config data --- src/App.tsx | 3 +- .../environment-variables-editor.tsx | 13 +- src/components/folder-selection-view.tsx | 3 +- src/components/message-part.tsx | 3 +- src/components/message-stream.tsx | 3 +- src/components/opencode-binary-selector.tsx | 15 ++- src/components/tool-call.tsx | 4 +- src/main.tsx | 9 +- .../{preferences.ts => preferences.tsx} | 120 +++++++++++++++--- 9 files changed, 136 insertions(+), 37 deletions(-) rename src/stores/{preferences.ts => preferences.tsx} (66%) diff --git a/src/App.tsx b/src/App.tsx index 36cea53e..9d2e5408 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,7 +29,7 @@ import { showFolderSelection, setShowFolderSelection, } from "./stores/ui" -import { toggleShowThinkingBlocks, preferences, addRecentFolder, setDiffViewMode } from "./stores/preferences" +import { useConfig } from "./stores/preferences" import { createInstance, instances, @@ -350,6 +350,7 @@ const ContextUsagePanel: Component<{ instanceId: string; sessionId: string }> = const App: Component = () => { const { isDark } = useTheme() + const { preferences, addRecentFolder, toggleShowThinkingBlocks, setDiffViewMode } = useConfig() const commandRegistry = createCommandRegistry() const [escapeInDebounce, setEscapeInDebounce] = createSignal(false) const [paletteCommands, setPaletteCommands] = createSignal([]) diff --git a/src/components/environment-variables-editor.tsx b/src/components/environment-variables-editor.tsx index 78d8e5e8..c4e07b1b 100644 --- a/src/components/environment-variables-editor.tsx +++ b/src/components/environment-variables-editor.tsx @@ -1,17 +1,18 @@ import { Component, createSignal, For, Show } from "solid-js" import { Plus, Trash2, Key, Globe } from "lucide-solid" -import { - preferences, - addEnvironmentVariable, - removeEnvironmentVariable, - updateEnvironmentVariables, -} from "../stores/preferences" +import { useConfig } from "../stores/preferences" interface EnvironmentVariablesEditorProps { disabled?: boolean } const EnvironmentVariablesEditor: Component = (props) => { + const { + preferences, + addEnvironmentVariable, + removeEnvironmentVariable, + updateEnvironmentVariables, + } = useConfig() const [envVars, setEnvVars] = createSignal>(preferences().environmentVariables || {}) const [newKey, setNewKey] = createSignal("") const [newValue, setNewValue] = createSignal("") diff --git a/src/components/folder-selection-view.tsx b/src/components/folder-selection-view.tsx index deae688f..18c42c75 100644 --- a/src/components/folder-selection-view.tsx +++ b/src/components/folder-selection-view.tsx @@ -1,6 +1,6 @@ import { Component, createSignal, Show, For, onMount, onCleanup, createEffect } from "solid-js" import { Folder, Clock, Trash2, FolderPlus, Settings, ChevronRight } from "lucide-solid" -import { recentFolders, removeRecentFolder, preferences, updateLastUsedBinary } from "../stores/preferences" +import { useConfig } from "../stores/preferences" import AdvancedSettingsModal from "./advanced-settings-modal" import Kbd from "./kbd" @@ -15,6 +15,7 @@ interface FolderSelectionViewProps { } const FolderSelectionView: Component = (props) => { + const { recentFolders, removeRecentFolder, preferences, updateLastUsedBinary } = useConfig() const [selectedIndex, setSelectedIndex] = createSignal(0) const [focusMode, setFocusMode] = createSignal<"recent" | "new" | null>("recent") const [selectedBinary, setSelectedBinary] = createSignal(preferences().lastUsedBinary || "opencode") diff --git a/src/components/message-part.tsx b/src/components/message-part.tsx index 93f016e7..d9e63f7e 100644 --- a/src/components/message-part.tsx +++ b/src/components/message-part.tsx @@ -3,7 +3,7 @@ import ToolCall from "./tool-call" import { isItemExpanded, toggleItemExpanded } from "../stores/tool-call-state" import { Markdown } from "./markdown" import { useTheme } from "../lib/theme" -import { preferences } from "../stores/preferences" +import { useConfig } from "../stores/preferences" import { partHasRenderableText, SDKPart, TextPart, ClientPart } from "../types/message" type ToolCallPart = Extract @@ -14,6 +14,7 @@ interface MessagePartProps { } export default function MessagePart(props: MessagePartProps) { const { isDark } = useTheme() + const { preferences } = useConfig() const partType = () => props.part?.type || "" const reasoningId = () => `reasoning-${props.part?.id || ""}` const isReasoningExpanded = () => isItemExpanded(reasoningId()) diff --git a/src/components/message-stream.tsx b/src/components/message-stream.tsx index 08da3b8c..3da7b987 100644 --- a/src/components/message-stream.tsx +++ b/src/components/message-stream.tsx @@ -30,7 +30,7 @@ import MessageItem from "./message-item" import ToolCall from "./tool-call" import { sseManager } from "../lib/sse-manager" import Kbd from "./kbd" -import { preferences } from "../stores/preferences" +import { useConfig } from "../stores/preferences" import { getSessionInfo, computeDisplayParts, sessions, setActiveSession, setActiveParentSession } from "../stores/sessions" import { setActiveInstanceId } from "../stores/instances" @@ -170,6 +170,7 @@ function getSessionCache(instanceId: string, sessionId: string): SessionCache { } export default function MessageStream(props: MessageStreamProps) { + const { preferences } = useConfig() let containerRef: HTMLDivElement | undefined const [autoScroll, setAutoScroll] = createSignal(true) const [showScrollBottomButton, setShowScrollBottomButton] = createSignal(false) diff --git a/src/components/opencode-binary-selector.tsx b/src/components/opencode-binary-selector.tsx index 3c33782b..a3668b4c 100644 --- a/src/components/opencode-binary-selector.tsx +++ b/src/components/opencode-binary-selector.tsx @@ -1,12 +1,6 @@ import { Component, For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js" import { FolderOpen, Trash2, Check, AlertCircle, Loader2, Plus } from "lucide-solid" -import { - opencodeBinaries, - addOpenCodeBinary, - removeOpenCodeBinary, - preferences, - updatePreferences, -} from "../stores/preferences" +import { useConfig } from "../stores/preferences" interface BinaryOption { path: string @@ -23,6 +17,13 @@ interface OpenCodeBinarySelectorProps { } const OpenCodeBinarySelector: Component = (props) => { + const { + opencodeBinaries, + addOpenCodeBinary, + removeOpenCodeBinary, + preferences, + updatePreferences, + } = useConfig() const [customPath, setCustomPath] = createSignal("") const [validating, setValidating] = createSignal(false) const [validationError, setValidationError] = createSignal(null) diff --git a/src/components/tool-call.tsx b/src/components/tool-call.tsx index 273bde7b..7c783240 100644 --- a/src/components/tool-call.tsx +++ b/src/components/tool-call.tsx @@ -6,7 +6,8 @@ import { useTheme } from "../lib/theme" import { getLanguageFromPath } from "../lib/markdown" import { isRenderableDiffText } from "../lib/diff-utils" import { getToolRenderCache, setToolRenderCache } from "../lib/tool-render-cache" -import { preferences, setDiffViewMode, type DiffViewMode } from "../stores/preferences" +import { useConfig } from "../stores/preferences" +import type { DiffViewMode } from "../stores/preferences" import type { TextPart, SDKPart, ClientPart } from "../types/message" type ToolCallPart = Extract @@ -177,6 +178,7 @@ function extractDiffPayload(toolName: string, state: ToolState): DiffPayload | n } export default function ToolCall(props: ToolCallProps) { + const { preferences, setDiffViewMode } = useConfig() const { isDark } = useTheme() const toolCallId = () => props.toolCallId || props.toolCall?.id || "" const expanded = () => isToolCallExpanded(toolCallId()) diff --git a/src/main.tsx b/src/main.tsx index ede61d2e..080336f0 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,7 @@ import { render } from "solid-js/web" import App from "./App" import { ThemeProvider } from "./lib/theme" +import { ConfigProvider } from "./stores/preferences" import "./index.css" import "@git-diff-view/solid/styles/diff-view-pure.css" @@ -12,9 +13,11 @@ if (!root) { render( () => ( - - - + + + + + ), root, ) diff --git a/src/stores/preferences.ts b/src/stores/preferences.tsx similarity index 66% rename from src/stores/preferences.ts rename to src/stores/preferences.tsx index 4e63e6ed..cea78690 100644 --- a/src/stores/preferences.ts +++ b/src/stores/preferences.tsx @@ -1,4 +1,5 @@ -import { createSignal, onMount } from "solid-js" +import { createContext, createSignal, onMount, useContext } from "solid-js" +import type { Accessor, ParentComponent } from "solid-js" import { storage, type ConfigData } from "../lib/storage" export interface ModelPreference { @@ -32,7 +33,7 @@ export interface RecentFolder { lastAccessed: number } -const MAX_RECENT_FOLDERS = 10 +const MAX_RECENT_FOLDERS = 20 const MAX_RECENT_MODELS = 5 const defaultPreferences: Preferences = { @@ -45,11 +46,13 @@ const defaultPreferences: Preferences = { const [preferences, setPreferences] = createSignal(defaultPreferences) const [recentFolders, setRecentFolders] = createSignal([]) const [opencodeBinaries, setOpenCodeBinaries] = createSignal([]) +const [isConfigLoaded, setIsConfigLoaded] = createSignal(false) let cachedConfig: ConfigData = { preferences: defaultPreferences, recentFolders: [], opencodeBinaries: [], } +let loadPromise: Promise | null = null async function loadConfig(): Promise { try { @@ -60,16 +63,25 @@ async function loadConfig(): Promise { recentFolders: config.recentFolders || [], opencodeBinaries: config.opencodeBinaries || [], } - setPreferences(cachedConfig.preferences) - setRecentFolders(cachedConfig.recentFolders) - setOpenCodeBinaries(cachedConfig.opencodeBinaries) } catch (error) { console.error("Failed to load config:", error) + cachedConfig = { + ...cachedConfig, + preferences: { ...defaultPreferences }, + recentFolders: [], + opencodeBinaries: [], + } } + + setPreferences(cachedConfig.preferences) + setRecentFolders(cachedConfig.recentFolders) + setOpenCodeBinaries(cachedConfig.opencodeBinaries) + setIsConfigLoaded(true) } async function saveConfig(): Promise { try { + await ensureConfigLoaded() const config: ConfigData = { ...cachedConfig, preferences: preferences(), @@ -83,6 +95,17 @@ async function saveConfig(): Promise { } } +async function ensureConfigLoaded(): Promise { + if (isConfigLoaded()) return + if (!loadPromise) { + loadPromise = loadConfig().finally(() => { + loadPromise = null + }) + } + await loadPromise +} + + function updatePreferences(updates: Partial): void { const updated = { ...preferences(), ...updates } setPreferences(updated) @@ -196,20 +219,85 @@ function getAgentModelPreference(instanceId: string, agent: string): ModelPrefer return preferences().agentModelSelections?.[instanceId]?.[agent] } -// Load config on mount and listen for changes from other instances -onMount(() => { - loadConfig() - - // Reload config when changed by another instance - const unsubscribe = storage.onConfigChanged(() => { - loadConfig() - }) - - // Cleanup on unmount - return unsubscribe +void ensureConfigLoaded().catch((error) => { + console.error("Failed to initialize config:", error) }) +interface ConfigContextValue { + isLoaded: Accessor + preferences: typeof preferences + recentFolders: typeof recentFolders + opencodeBinaries: typeof opencodeBinaries + toggleShowThinkingBlocks: typeof toggleShowThinkingBlocks + setDiffViewMode: typeof setDiffViewMode + addRecentFolder: typeof addRecentFolder + removeRecentFolder: typeof removeRecentFolder + addOpenCodeBinary: typeof addOpenCodeBinary + removeOpenCodeBinary: typeof removeOpenCodeBinary + updateLastUsedBinary: typeof updateLastUsedBinary + updatePreferences: typeof updatePreferences + updateEnvironmentVariables: typeof updateEnvironmentVariables + addEnvironmentVariable: typeof addEnvironmentVariable + removeEnvironmentVariable: typeof removeEnvironmentVariable + addRecentModelPreference: typeof addRecentModelPreference + setAgentModelPreference: typeof setAgentModelPreference + getAgentModelPreference: typeof getAgentModelPreference +} + +const ConfigContext = createContext() + +const configContextValue: ConfigContextValue = { + isLoaded: isConfigLoaded, + preferences, + recentFolders, + opencodeBinaries, + toggleShowThinkingBlocks, + setDiffViewMode, + addRecentFolder, + removeRecentFolder, + addOpenCodeBinary, + removeOpenCodeBinary, + updateLastUsedBinary, + updatePreferences, + updateEnvironmentVariables, + addEnvironmentVariable, + removeEnvironmentVariable, + addRecentModelPreference, + setAgentModelPreference, + getAgentModelPreference, +} + +const ConfigProvider: ParentComponent = (props) => { + onMount(() => { + ensureConfigLoaded().catch((error) => { + console.error("Failed to initialize config:", error) + }) + + const unsubscribe = storage.onConfigChanged(() => { + loadConfig().catch((error) => { + console.error("Failed to refresh config:", error) + }) + }) + + return () => { + unsubscribe() + } + }) + + return {props.children} +} + +function useConfig(): ConfigContextValue { + const context = useContext(ConfigContext) + if (!context) { + throw new Error("useConfig must be used within ConfigProvider") + } + return context +} + export { + ConfigProvider, + useConfig, preferences, updatePreferences, toggleShowThinkingBlocks,