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
This commit is contained in:
Shantur Rathore
2025-11-14 20:42:13 +00:00
parent 6fa41d51be
commit efe7af6f77
9 changed files with 136 additions and 37 deletions

View File

@@ -29,7 +29,7 @@ import {
showFolderSelection, showFolderSelection,
setShowFolderSelection, setShowFolderSelection,
} from "./stores/ui" } from "./stores/ui"
import { toggleShowThinkingBlocks, preferences, addRecentFolder, setDiffViewMode } from "./stores/preferences" import { useConfig } from "./stores/preferences"
import { import {
createInstance, createInstance,
instances, instances,
@@ -350,6 +350,7 @@ const ContextUsagePanel: Component<{ instanceId: string; sessionId: string }> =
const App: Component = () => { const App: Component = () => {
const { isDark } = useTheme() const { isDark } = useTheme()
const { preferences, addRecentFolder, toggleShowThinkingBlocks, setDiffViewMode } = useConfig()
const commandRegistry = createCommandRegistry() const commandRegistry = createCommandRegistry()
const [escapeInDebounce, setEscapeInDebounce] = createSignal(false) const [escapeInDebounce, setEscapeInDebounce] = createSignal(false)
const [paletteCommands, setPaletteCommands] = createSignal<Command[]>([]) const [paletteCommands, setPaletteCommands] = createSignal<Command[]>([])

View File

@@ -1,17 +1,18 @@
import { Component, createSignal, For, Show } from "solid-js" import { Component, createSignal, For, Show } from "solid-js"
import { Plus, Trash2, Key, Globe } from "lucide-solid" import { Plus, Trash2, Key, Globe } from "lucide-solid"
import { import { useConfig } from "../stores/preferences"
preferences,
addEnvironmentVariable,
removeEnvironmentVariable,
updateEnvironmentVariables,
} from "../stores/preferences"
interface EnvironmentVariablesEditorProps { interface EnvironmentVariablesEditorProps {
disabled?: boolean disabled?: boolean
} }
const EnvironmentVariablesEditor: Component<EnvironmentVariablesEditorProps> = (props) => { const EnvironmentVariablesEditor: Component<EnvironmentVariablesEditorProps> = (props) => {
const {
preferences,
addEnvironmentVariable,
removeEnvironmentVariable,
updateEnvironmentVariables,
} = useConfig()
const [envVars, setEnvVars] = createSignal<Record<string, string>>(preferences().environmentVariables || {}) const [envVars, setEnvVars] = createSignal<Record<string, string>>(preferences().environmentVariables || {})
const [newKey, setNewKey] = createSignal("") const [newKey, setNewKey] = createSignal("")
const [newValue, setNewValue] = createSignal("") const [newValue, setNewValue] = createSignal("")

View File

@@ -1,6 +1,6 @@
import { Component, createSignal, Show, For, onMount, onCleanup, createEffect } from "solid-js" import { Component, createSignal, Show, For, onMount, onCleanup, createEffect } from "solid-js"
import { Folder, Clock, Trash2, FolderPlus, Settings, ChevronRight } from "lucide-solid" 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 AdvancedSettingsModal from "./advanced-settings-modal"
import Kbd from "./kbd" import Kbd from "./kbd"
@@ -15,6 +15,7 @@ interface FolderSelectionViewProps {
} }
const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => { const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
const { recentFolders, removeRecentFolder, preferences, updateLastUsedBinary } = useConfig()
const [selectedIndex, setSelectedIndex] = createSignal(0) const [selectedIndex, setSelectedIndex] = createSignal(0)
const [focusMode, setFocusMode] = createSignal<"recent" | "new" | null>("recent") const [focusMode, setFocusMode] = createSignal<"recent" | "new" | null>("recent")
const [selectedBinary, setSelectedBinary] = createSignal(preferences().lastUsedBinary || "opencode") const [selectedBinary, setSelectedBinary] = createSignal(preferences().lastUsedBinary || "opencode")

View File

@@ -3,7 +3,7 @@ import ToolCall from "./tool-call"
import { isItemExpanded, toggleItemExpanded } from "../stores/tool-call-state" import { isItemExpanded, toggleItemExpanded } from "../stores/tool-call-state"
import { Markdown } from "./markdown" import { Markdown } from "./markdown"
import { useTheme } from "../lib/theme" import { useTheme } from "../lib/theme"
import { preferences } from "../stores/preferences" import { useConfig } from "../stores/preferences"
import { partHasRenderableText, SDKPart, TextPart, ClientPart } from "../types/message" import { partHasRenderableText, SDKPart, TextPart, ClientPart } from "../types/message"
type ToolCallPart = Extract<ClientPart, { type: "tool" }> type ToolCallPart = Extract<ClientPart, { type: "tool" }>
@@ -14,6 +14,7 @@ interface MessagePartProps {
} }
export default function MessagePart(props: MessagePartProps) { export default function MessagePart(props: MessagePartProps) {
const { isDark } = useTheme() const { isDark } = useTheme()
const { preferences } = useConfig()
const partType = () => props.part?.type || "" const partType = () => props.part?.type || ""
const reasoningId = () => `reasoning-${props.part?.id || ""}` const reasoningId = () => `reasoning-${props.part?.id || ""}`
const isReasoningExpanded = () => isItemExpanded(reasoningId()) const isReasoningExpanded = () => isItemExpanded(reasoningId())

View File

@@ -30,7 +30,7 @@ import MessageItem from "./message-item"
import ToolCall from "./tool-call" import ToolCall from "./tool-call"
import { sseManager } from "../lib/sse-manager" import { sseManager } from "../lib/sse-manager"
import Kbd from "./kbd" import Kbd from "./kbd"
import { preferences } from "../stores/preferences" import { useConfig } from "../stores/preferences"
import { getSessionInfo, computeDisplayParts, sessions, setActiveSession, setActiveParentSession } from "../stores/sessions" import { getSessionInfo, computeDisplayParts, sessions, setActiveSession, setActiveParentSession } from "../stores/sessions"
import { setActiveInstanceId } from "../stores/instances" import { setActiveInstanceId } from "../stores/instances"
@@ -170,6 +170,7 @@ function getSessionCache(instanceId: string, sessionId: string): SessionCache {
} }
export default function MessageStream(props: MessageStreamProps) { export default function MessageStream(props: MessageStreamProps) {
const { preferences } = useConfig()
let containerRef: HTMLDivElement | undefined let containerRef: HTMLDivElement | undefined
const [autoScroll, setAutoScroll] = createSignal(true) const [autoScroll, setAutoScroll] = createSignal(true)
const [showScrollBottomButton, setShowScrollBottomButton] = createSignal(false) const [showScrollBottomButton, setShowScrollBottomButton] = createSignal(false)

View File

@@ -1,12 +1,6 @@
import { Component, For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js" import { Component, For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
import { FolderOpen, Trash2, Check, AlertCircle, Loader2, Plus } from "lucide-solid" import { FolderOpen, Trash2, Check, AlertCircle, Loader2, Plus } from "lucide-solid"
import { import { useConfig } from "../stores/preferences"
opencodeBinaries,
addOpenCodeBinary,
removeOpenCodeBinary,
preferences,
updatePreferences,
} from "../stores/preferences"
interface BinaryOption { interface BinaryOption {
path: string path: string
@@ -23,6 +17,13 @@ interface OpenCodeBinarySelectorProps {
} }
const OpenCodeBinarySelector: Component<OpenCodeBinarySelectorProps> = (props) => { const OpenCodeBinarySelector: Component<OpenCodeBinarySelectorProps> = (props) => {
const {
opencodeBinaries,
addOpenCodeBinary,
removeOpenCodeBinary,
preferences,
updatePreferences,
} = useConfig()
const [customPath, setCustomPath] = createSignal("") const [customPath, setCustomPath] = createSignal("")
const [validating, setValidating] = createSignal(false) const [validating, setValidating] = createSignal(false)
const [validationError, setValidationError] = createSignal<string | null>(null) const [validationError, setValidationError] = createSignal<string | null>(null)

View File

@@ -6,7 +6,8 @@ import { useTheme } from "../lib/theme"
import { getLanguageFromPath } from "../lib/markdown" import { getLanguageFromPath } from "../lib/markdown"
import { isRenderableDiffText } from "../lib/diff-utils" import { isRenderableDiffText } from "../lib/diff-utils"
import { getToolRenderCache, setToolRenderCache } from "../lib/tool-render-cache" 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" import type { TextPart, SDKPart, ClientPart } from "../types/message"
type ToolCallPart = Extract<ClientPart, { type: "tool" }> type ToolCallPart = Extract<ClientPart, { type: "tool" }>
@@ -177,6 +178,7 @@ function extractDiffPayload(toolName: string, state: ToolState): DiffPayload | n
} }
export default function ToolCall(props: ToolCallProps) { export default function ToolCall(props: ToolCallProps) {
const { preferences, setDiffViewMode } = useConfig()
const { isDark } = useTheme() const { isDark } = useTheme()
const toolCallId = () => props.toolCallId || props.toolCall?.id || "" const toolCallId = () => props.toolCallId || props.toolCall?.id || ""
const expanded = () => isToolCallExpanded(toolCallId()) const expanded = () => isToolCallExpanded(toolCallId())

View File

@@ -1,6 +1,7 @@
import { render } from "solid-js/web" import { render } from "solid-js/web"
import App from "./App" import App from "./App"
import { ThemeProvider } from "./lib/theme" import { ThemeProvider } from "./lib/theme"
import { ConfigProvider } from "./stores/preferences"
import "./index.css" import "./index.css"
import "@git-diff-view/solid/styles/diff-view-pure.css" import "@git-diff-view/solid/styles/diff-view-pure.css"
@@ -12,9 +13,11 @@ if (!root) {
render( render(
() => ( () => (
<ThemeProvider> <ConfigProvider>
<App /> <ThemeProvider>
</ThemeProvider> <App />
</ThemeProvider>
</ConfigProvider>
), ),
root, root,
) )

View File

@@ -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" import { storage, type ConfigData } from "../lib/storage"
export interface ModelPreference { export interface ModelPreference {
@@ -32,7 +33,7 @@ export interface RecentFolder {
lastAccessed: number lastAccessed: number
} }
const MAX_RECENT_FOLDERS = 10 const MAX_RECENT_FOLDERS = 20
const MAX_RECENT_MODELS = 5 const MAX_RECENT_MODELS = 5
const defaultPreferences: Preferences = { const defaultPreferences: Preferences = {
@@ -45,11 +46,13 @@ const defaultPreferences: Preferences = {
const [preferences, setPreferences] = createSignal<Preferences>(defaultPreferences) const [preferences, setPreferences] = createSignal<Preferences>(defaultPreferences)
const [recentFolders, setRecentFolders] = createSignal<RecentFolder[]>([]) const [recentFolders, setRecentFolders] = createSignal<RecentFolder[]>([])
const [opencodeBinaries, setOpenCodeBinaries] = createSignal<OpenCodeBinary[]>([]) const [opencodeBinaries, setOpenCodeBinaries] = createSignal<OpenCodeBinary[]>([])
const [isConfigLoaded, setIsConfigLoaded] = createSignal(false)
let cachedConfig: ConfigData = { let cachedConfig: ConfigData = {
preferences: defaultPreferences, preferences: defaultPreferences,
recentFolders: [], recentFolders: [],
opencodeBinaries: [], opencodeBinaries: [],
} }
let loadPromise: Promise<void> | null = null
async function loadConfig(): Promise<void> { async function loadConfig(): Promise<void> {
try { try {
@@ -60,16 +63,25 @@ async function loadConfig(): Promise<void> {
recentFolders: config.recentFolders || [], recentFolders: config.recentFolders || [],
opencodeBinaries: config.opencodeBinaries || [], opencodeBinaries: config.opencodeBinaries || [],
} }
setPreferences(cachedConfig.preferences)
setRecentFolders(cachedConfig.recentFolders)
setOpenCodeBinaries(cachedConfig.opencodeBinaries)
} catch (error) { } catch (error) {
console.error("Failed to load config:", 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<void> { async function saveConfig(): Promise<void> {
try { try {
await ensureConfigLoaded()
const config: ConfigData = { const config: ConfigData = {
...cachedConfig, ...cachedConfig,
preferences: preferences(), preferences: preferences(),
@@ -83,6 +95,17 @@ async function saveConfig(): Promise<void> {
} }
} }
async function ensureConfigLoaded(): Promise<void> {
if (isConfigLoaded()) return
if (!loadPromise) {
loadPromise = loadConfig().finally(() => {
loadPromise = null
})
}
await loadPromise
}
function updatePreferences(updates: Partial<Preferences>): void { function updatePreferences(updates: Partial<Preferences>): void {
const updated = { ...preferences(), ...updates } const updated = { ...preferences(), ...updates }
setPreferences(updated) setPreferences(updated)
@@ -196,20 +219,85 @@ function getAgentModelPreference(instanceId: string, agent: string): ModelPrefer
return preferences().agentModelSelections?.[instanceId]?.[agent] return preferences().agentModelSelections?.[instanceId]?.[agent]
} }
// Load config on mount and listen for changes from other instances void ensureConfigLoaded().catch((error) => {
onMount(() => { console.error("Failed to initialize config:", error)
loadConfig()
// Reload config when changed by another instance
const unsubscribe = storage.onConfigChanged(() => {
loadConfig()
})
// Cleanup on unmount
return unsubscribe
}) })
interface ConfigContextValue {
isLoaded: Accessor<boolean>
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<ConfigContextValue>()
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 <ConfigContext.Provider value={configContextValue}>{props.children}</ConfigContext.Provider>
}
function useConfig(): ConfigContextValue {
const context = useContext(ConfigContext)
if (!context) {
throw new Error("useConfig must be used within ConfigProvider")
}
return context
}
export { export {
ConfigProvider,
useConfig,
preferences, preferences,
updatePreferences, updatePreferences,
toggleShowThinkingBlocks, toggleShowThinkingBlocks,