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:
@@ -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[]>([])
|
||||||
|
|||||||
@@ -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("")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
Reference in New Issue
Block a user