refine config provider and full replacement flow
This commit is contained in:
@@ -6,7 +6,7 @@ import {
|
|||||||
} from "../api-types"
|
} from "../api-types"
|
||||||
import { ConfigStore } from "./store"
|
import { ConfigStore } from "./store"
|
||||||
import { EventBus } from "../events/bus"
|
import { EventBus } from "../events/bus"
|
||||||
import type { ConfigFileUpdate } from "./schema"
|
import type { ConfigFile } from "./schema"
|
||||||
import { Logger } from "../logger"
|
import { Logger } from "../logger"
|
||||||
|
|
||||||
export class BinaryRegistry {
|
export class BinaryRegistry {
|
||||||
@@ -39,17 +39,15 @@ export class BinaryRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = this.configStore.get()
|
const config = this.configStore.get()
|
||||||
const deduped = config.opencodeBinaries.filter((binary) => binary.path !== request.path)
|
const nextConfig = this.cloneConfig(config)
|
||||||
|
const deduped = nextConfig.opencodeBinaries.filter((binary) => binary.path !== request.path)
|
||||||
const update: ConfigFileUpdate = {
|
nextConfig.opencodeBinaries = [entry, ...deduped]
|
||||||
opencodeBinaries: [entry, ...deduped],
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.makeDefault) {
|
if (request.makeDefault) {
|
||||||
update.preferences = { lastUsedBinary: request.path }
|
nextConfig.preferences.lastUsedBinary = request.path
|
||||||
}
|
}
|
||||||
|
|
||||||
this.configStore.update(update)
|
this.configStore.replace(nextConfig)
|
||||||
const record = this.getById(request.path)
|
const record = this.getById(request.path)
|
||||||
this.emitChange()
|
this.emitChange()
|
||||||
return record
|
return record
|
||||||
@@ -58,19 +56,16 @@ export class BinaryRegistry {
|
|||||||
update(id: string, updates: BinaryUpdateRequest): BinaryRecord {
|
update(id: string, updates: BinaryUpdateRequest): BinaryRecord {
|
||||||
this.logger.debug({ id }, "Updating OpenCode binary")
|
this.logger.debug({ id }, "Updating OpenCode binary")
|
||||||
const config = this.configStore.get()
|
const config = this.configStore.get()
|
||||||
const updatedEntries = config.opencodeBinaries.map((binary) =>
|
const nextConfig = this.cloneConfig(config)
|
||||||
|
nextConfig.opencodeBinaries = nextConfig.opencodeBinaries.map((binary) =>
|
||||||
binary.path === id ? { ...binary, label: updates.label ?? binary.label } : binary,
|
binary.path === id ? { ...binary, label: updates.label ?? binary.label } : binary,
|
||||||
)
|
)
|
||||||
|
|
||||||
const update: ConfigFileUpdate = {
|
|
||||||
opencodeBinaries: updatedEntries,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updates.makeDefault) {
|
if (updates.makeDefault) {
|
||||||
update.preferences = { lastUsedBinary: id }
|
nextConfig.preferences.lastUsedBinary = id
|
||||||
}
|
}
|
||||||
|
|
||||||
this.configStore.update(update)
|
this.configStore.replace(nextConfig)
|
||||||
const record = this.getById(id)
|
const record = this.getById(id)
|
||||||
this.emitChange()
|
this.emitChange()
|
||||||
return record
|
return record
|
||||||
@@ -79,14 +74,15 @@ export class BinaryRegistry {
|
|||||||
remove(id: string) {
|
remove(id: string) {
|
||||||
this.logger.debug({ id }, "Removing OpenCode binary")
|
this.logger.debug({ id }, "Removing OpenCode binary")
|
||||||
const config = this.configStore.get()
|
const config = this.configStore.get()
|
||||||
const remaining = config.opencodeBinaries.filter((binary) => binary.path !== id)
|
const nextConfig = this.cloneConfig(config)
|
||||||
const update: ConfigFileUpdate = { opencodeBinaries: remaining }
|
const remaining = nextConfig.opencodeBinaries.filter((binary) => binary.path !== id)
|
||||||
|
nextConfig.opencodeBinaries = remaining
|
||||||
|
|
||||||
if (config.preferences.lastUsedBinary === id) {
|
if (nextConfig.preferences.lastUsedBinary === id) {
|
||||||
update.preferences = { lastUsedBinary: remaining[0]?.path }
|
nextConfig.preferences.lastUsedBinary = remaining[0]?.path
|
||||||
}
|
}
|
||||||
|
|
||||||
this.configStore.update(update)
|
this.configStore.replace(nextConfig)
|
||||||
this.emitChange()
|
this.emitChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +96,12 @@ export class BinaryRegistry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private cloneConfig(config: ConfigFile): ConfigFile {
|
||||||
|
return JSON.parse(JSON.stringify(config)) as ConfigFile
|
||||||
|
}
|
||||||
|
|
||||||
private mapRecords(): BinaryRecord[] {
|
private mapRecords(): BinaryRecord[] {
|
||||||
|
|
||||||
const config = this.configStore.get()
|
const config = this.configStore.get()
|
||||||
const configuredBinaries = config.opencodeBinaries.map<BinaryRecord>((binary) => ({
|
const configuredBinaries = config.opencodeBinaries.map<BinaryRecord>((binary) => ({
|
||||||
id: binary.path,
|
id: binary.path,
|
||||||
|
|||||||
@@ -19,17 +19,6 @@ const PreferencesSchema = z.object({
|
|||||||
diagnosticsExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
|
diagnosticsExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
|
||||||
})
|
})
|
||||||
|
|
||||||
const PreferencesUpdateSchema = z.object({
|
|
||||||
showThinkingBlocks: z.boolean().optional(),
|
|
||||||
lastUsedBinary: z.string().optional(),
|
|
||||||
environmentVariables: z.record(z.string()).optional(),
|
|
||||||
modelRecents: z.array(ModelPreferenceSchema).optional(),
|
|
||||||
agentModelSelections: AgentModelSelectionsSchema.optional(),
|
|
||||||
diffViewMode: z.enum(["split", "unified"]).optional(),
|
|
||||||
toolOutputExpansion: z.enum(["expanded", "collapsed"]).optional(),
|
|
||||||
diagnosticsExpansion: z.enum(["expanded", "collapsed"]).optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const RecentFolderSchema = z.object({
|
const RecentFolderSchema = z.object({
|
||||||
path: z.string(),
|
path: z.string(),
|
||||||
lastAccessed: z.number().nonnegative(),
|
lastAccessed: z.number().nonnegative(),
|
||||||
@@ -49,13 +38,6 @@ const ConfigFileSchema = z.object({
|
|||||||
theme: z.enum(["light", "dark", "system"]).optional(),
|
theme: z.enum(["light", "dark", "system"]).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const ConfigFileUpdateSchema = z.object({
|
|
||||||
preferences: PreferencesUpdateSchema.optional(),
|
|
||||||
recentFolders: z.array(RecentFolderSchema).optional(),
|
|
||||||
opencodeBinaries: z.array(OpenCodeBinarySchema).optional(),
|
|
||||||
theme: z.enum(["light", "dark", "system"]).optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const DEFAULT_CONFIG = ConfigFileSchema.parse({})
|
const DEFAULT_CONFIG = ConfigFileSchema.parse({})
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -66,7 +48,6 @@ export {
|
|||||||
RecentFolderSchema,
|
RecentFolderSchema,
|
||||||
OpenCodeBinarySchema,
|
OpenCodeBinarySchema,
|
||||||
ConfigFileSchema,
|
ConfigFileSchema,
|
||||||
ConfigFileUpdateSchema,
|
|
||||||
DEFAULT_CONFIG,
|
DEFAULT_CONFIG,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,4 +58,3 @@ export type Preferences = z.infer<typeof PreferencesSchema>
|
|||||||
export type RecentFolder = z.infer<typeof RecentFolderSchema>
|
export type RecentFolder = z.infer<typeof RecentFolderSchema>
|
||||||
export type OpenCodeBinary = z.infer<typeof OpenCodeBinarySchema>
|
export type OpenCodeBinary = z.infer<typeof OpenCodeBinarySchema>
|
||||||
export type ConfigFile = z.infer<typeof ConfigFileSchema>
|
export type ConfigFile = z.infer<typeof ConfigFileSchema>
|
||||||
export type ConfigFileUpdate = z.infer<typeof ConfigFileUpdateSchema>
|
|
||||||
|
|||||||
@@ -2,14 +2,7 @@ import fs from "fs"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { EventBus } from "../events/bus"
|
import { EventBus } from "../events/bus"
|
||||||
import { Logger } from "../logger"
|
import { Logger } from "../logger"
|
||||||
import {
|
import { ConfigFile, ConfigFileSchema, DEFAULT_CONFIG } from "./schema"
|
||||||
AgentModelSelections,
|
|
||||||
ConfigFile,
|
|
||||||
ConfigFileUpdate,
|
|
||||||
ConfigFileSchema,
|
|
||||||
ConfigFileUpdateSchema,
|
|
||||||
DEFAULT_CONFIG,
|
|
||||||
} from "./schema"
|
|
||||||
|
|
||||||
export class ConfigStore {
|
export class ConfigStore {
|
||||||
private cache: ConfigFile = DEFAULT_CONFIG
|
private cache: ConfigFile = DEFAULT_CONFIG
|
||||||
@@ -50,54 +43,18 @@ export class ConfigStore {
|
|||||||
return this.load()
|
return this.load()
|
||||||
}
|
}
|
||||||
|
|
||||||
update(partial: ConfigFile | ConfigFileUpdate) {
|
replace(config: ConfigFile) {
|
||||||
const safePartial =
|
const validated = ConfigFileSchema.parse(config)
|
||||||
"recentFolders" in partial && "opencodeBinaries" in partial
|
this.commit(validated)
|
||||||
? ConfigFileSchema.parse(partial)
|
}
|
||||||
: ConfigFileUpdateSchema.parse(partial ?? {})
|
|
||||||
const merged = this.mergeConfig(this.load(), safePartial)
|
private commit(next: ConfigFile) {
|
||||||
this.cache = ConfigFileSchema.parse(merged)
|
this.cache = next
|
||||||
|
this.loaded = true
|
||||||
this.persist()
|
this.persist()
|
||||||
this.eventBus?.publish({ type: "config.appChanged", config: this.cache })
|
this.eventBus?.publish({ type: "config.appChanged", config: this.cache })
|
||||||
this.logger.debug("Config updated")
|
this.logger.info("Config updated")
|
||||||
}
|
this.logger.debug({ config: this.cache }, "Config payload")
|
||||||
|
|
||||||
private mergeConfig(current: ConfigFile, partial: ConfigFile | ConfigFileUpdate): ConfigFile {
|
|
||||||
const mergedPreferences = {
|
|
||||||
...current.preferences,
|
|
||||||
...partial.preferences,
|
|
||||||
environmentVariables: {
|
|
||||||
...current.preferences.environmentVariables,
|
|
||||||
...(partial.preferences?.environmentVariables ?? {}),
|
|
||||||
},
|
|
||||||
agentModelSelections: this.mergeAgentSelections(
|
|
||||||
current.preferences.agentModelSelections,
|
|
||||||
partial.preferences?.agentModelSelections,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...current,
|
|
||||||
...partial,
|
|
||||||
preferences: mergedPreferences,
|
|
||||||
recentFolders: partial.recentFolders ?? current.recentFolders,
|
|
||||||
opencodeBinaries: partial.opencodeBinaries ?? current.opencodeBinaries,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private mergeAgentSelections(base: AgentModelSelections, update?: AgentModelSelections) {
|
|
||||||
if (!update) {
|
|
||||||
return base
|
|
||||||
}
|
|
||||||
|
|
||||||
const result: AgentModelSelections = { ...base }
|
|
||||||
for (const [instanceId, agentMap] of Object.entries(update)) {
|
|
||||||
result[instanceId] = {
|
|
||||||
...(base[instanceId] ?? {}),
|
|
||||||
...agentMap,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private persist() {
|
private persist() {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { FastifyInstance } from "fastify"
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { ConfigStore } from "../../config/store"
|
import { ConfigStore } from "../../config/store"
|
||||||
import { BinaryRegistry } from "../../config/binaries"
|
import { BinaryRegistry } from "../../config/binaries"
|
||||||
import { ConfigFileSchema, ConfigFileUpdateSchema } from "../../config/schema"
|
import { ConfigFileSchema } from "../../config/schema"
|
||||||
|
|
||||||
interface RouteDeps {
|
interface RouteDeps {
|
||||||
configStore: ConfigStore
|
configStore: ConfigStore
|
||||||
@@ -29,13 +29,7 @@ export function registerConfigRoutes(app: FastifyInstance, deps: RouteDeps) {
|
|||||||
|
|
||||||
app.put("/api/config/app", async (request) => {
|
app.put("/api/config/app", async (request) => {
|
||||||
const body = ConfigFileSchema.parse(request.body ?? {})
|
const body = ConfigFileSchema.parse(request.body ?? {})
|
||||||
deps.configStore.update(body)
|
deps.configStore.replace(body)
|
||||||
return deps.configStore.get()
|
|
||||||
})
|
|
||||||
|
|
||||||
app.patch("/api/config/app", async (request) => {
|
|
||||||
const body = ConfigFileUpdateSchema.parse(request.body ?? {})
|
|
||||||
deps.configStore.update(body)
|
|
||||||
return deps.configStore.get()
|
return deps.configStore.get()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const App: Component = () => {
|
|||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme()
|
||||||
const {
|
const {
|
||||||
preferences,
|
preferences,
|
||||||
addRecentFolder,
|
recordWorkspaceLaunch,
|
||||||
toggleShowThinkingBlocks,
|
toggleShowThinkingBlocks,
|
||||||
setDiffViewMode,
|
setDiffViewMode,
|
||||||
setToolOutputExpansion,
|
setToolOutputExpansion,
|
||||||
@@ -92,7 +92,7 @@ const App: Component = () => {
|
|||||||
setIsSelectingFolder(true)
|
setIsSelectingFolder(true)
|
||||||
const selectedBinary = binaryPath || preferences().lastUsedBinary || "opencode"
|
const selectedBinary = binaryPath || preferences().lastUsedBinary || "opencode"
|
||||||
try {
|
try {
|
||||||
addRecentFolder(folderPath)
|
recordWorkspaceLaunch(folderPath, selectedBinary)
|
||||||
clearLaunchError()
|
clearLaunchError()
|
||||||
const instanceId = await createInstance(folderPath, selectedBinary)
|
const instanceId = await createInstance(folderPath, selectedBinary)
|
||||||
setHasInstances(true)
|
setHasInstances(true)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ interface FolderSelectionViewProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
||||||
const { recentFolders, removeRecentFolder, preferences, updateLastUsedBinary } = useConfig()
|
const { recentFolders, removeRecentFolder, preferences } = 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")
|
||||||
@@ -169,7 +169,6 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
|
|
||||||
function handleFolderSelect(path: string) {
|
function handleFolderSelect(path: string) {
|
||||||
if (isLoading()) return
|
if (isLoading()) return
|
||||||
updateLastUsedBinary(selectedBinary())
|
|
||||||
props.onSelectFolder(path, selectedBinary())
|
props.onSelectFolder(path, selectedBinary())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
AppConfig,
|
AppConfig,
|
||||||
AppConfigUpdateRequest,
|
|
||||||
BinaryCreateRequest,
|
BinaryCreateRequest,
|
||||||
BinaryListResponse,
|
BinaryListResponse,
|
||||||
BinaryUpdateRequest,
|
BinaryUpdateRequest,
|
||||||
@@ -115,12 +114,6 @@ export const cliApi = {
|
|||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
patchConfig(payload: AppConfigUpdateRequest): Promise<AppConfig> {
|
|
||||||
return request<AppConfig>("/api/config/app", {
|
|
||||||
method: "PATCH",
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
listBinaries(): Promise<BinaryListResponse> {
|
listBinaries(): Promise<BinaryListResponse> {
|
||||||
return request<BinaryListResponse>("/api/config/binaries")
|
return request<BinaryListResponse>("/api/config/binaries")
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,21 +4,58 @@ import { cliEvents } from "./cli-events"
|
|||||||
|
|
||||||
export type ConfigData = AppConfig
|
export type ConfigData = AppConfig
|
||||||
|
|
||||||
|
function isDeepEqual(a: unknown, b: unknown): boolean {
|
||||||
|
if (a === b) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof a === "object" && a !== null && typeof b === "object" && b !== null) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(a) === JSON.stringify(b)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to compare config objects", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
export class ServerStorage {
|
export class ServerStorage {
|
||||||
private configChangeListeners: Set<() => void> = new Set()
|
private configChangeListeners: Set<(config: ConfigData) => void> = new Set()
|
||||||
|
private configCache: ConfigData | null = null
|
||||||
|
private loadPromise: Promise<ConfigData> | null = null
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
cliEvents.on("config.appChanged", () => this.notifyConfigChanged())
|
cliEvents.on("config.appChanged", (event) => {
|
||||||
|
if (event.type !== "config.appChanged") return
|
||||||
|
this.setConfigCache(event.config)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadConfig(): Promise<ConfigData> {
|
async loadConfig(): Promise<ConfigData> {
|
||||||
const config = await cliApi.fetchConfig()
|
if (this.configCache) {
|
||||||
return config
|
return this.configCache
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.loadPromise) {
|
||||||
|
this.loadPromise = cliApi
|
||||||
|
.fetchConfig()
|
||||||
|
.then((config) => {
|
||||||
|
this.setConfigCache(config)
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loadPromise = null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.loadPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveConfig(config: ConfigData): Promise<void> {
|
async updateConfig(next: ConfigData): Promise<ConfigData> {
|
||||||
await cliApi.updateConfig(config)
|
const nextConfig = await cliApi.updateConfig(next)
|
||||||
|
this.setConfigCache(nextConfig)
|
||||||
|
return nextConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadInstanceData(instanceId: string): Promise<InstanceData> {
|
async loadInstanceData(instanceId: string): Promise<InstanceData> {
|
||||||
@@ -33,14 +70,26 @@ export class ServerStorage {
|
|||||||
await cliApi.deleteInstanceData(instanceId)
|
await cliApi.deleteInstanceData(instanceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
onConfigChanged(listener: () => void): () => void {
|
onConfigChanged(listener: (config: ConfigData) => void): () => void {
|
||||||
this.configChangeListeners.add(listener)
|
this.configChangeListeners.add(listener)
|
||||||
|
if (this.configCache) {
|
||||||
|
listener(this.configCache)
|
||||||
|
}
|
||||||
return () => this.configChangeListeners.delete(listener)
|
return () => this.configChangeListeners.delete(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
private notifyConfigChanged() {
|
private setConfigCache(config: ConfigData) {
|
||||||
|
if (this.configCache && isDeepEqual(this.configCache, config)) {
|
||||||
|
this.configCache = config
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.configCache = config
|
||||||
|
this.notifyConfigChanged(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
private notifyConfigChanged(config: ConfigData) {
|
||||||
for (const listener of this.configChangeListeners) {
|
for (const listener of this.configChangeListeners) {
|
||||||
listener()
|
listener(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createContext, createSignal, useContext, onMount, createEffect, type JSX } from "solid-js"
|
import { createContext, createEffect, createSignal, onMount, useContext, type JSX } from "solid-js"
|
||||||
import { storage, type ConfigData } from "./storage"
|
import { useConfig } from "../stores/preferences"
|
||||||
|
|
||||||
interface ThemeContextValue {
|
interface ThemeContextValue {
|
||||||
isDark: () => boolean
|
isDark: () => boolean
|
||||||
@@ -20,64 +20,30 @@ function applyTheme(dark: boolean) {
|
|||||||
|
|
||||||
export function ThemeProvider(props: { children: JSX.Element }) {
|
export function ThemeProvider(props: { children: JSX.Element }) {
|
||||||
const systemPrefersDark = window.matchMedia("(prefers-color-scheme: dark)")
|
const systemPrefersDark = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
const [isDark, setIsDarkSignal] = createSignal(true) //systemPrefersDark.matches)
|
const { themePreference, setThemePreference } = useConfig()
|
||||||
let themePreference: "system" | "dark" | "light" = "dark"
|
const [isDark, setIsDarkSignal] = createSignal(false)
|
||||||
|
|
||||||
applyTheme(true) //systemPrefersDark.matches)
|
const resolveDarkTheme = () => {
|
||||||
|
const preference = themePreference()
|
||||||
async function loadTheme() {
|
if (preference === "system") {
|
||||||
try {
|
return systemPrefersDark.matches
|
||||||
const config = await storage.loadConfig()
|
|
||||||
const savedTheme = config.theme
|
|
||||||
let themeDark: boolean
|
|
||||||
|
|
||||||
if (savedTheme === "system") {
|
|
||||||
themePreference = "system"
|
|
||||||
themeDark = systemPrefersDark.matches
|
|
||||||
} else if (savedTheme === "dark") {
|
|
||||||
themePreference = "dark"
|
|
||||||
themeDark = true
|
|
||||||
} else if (savedTheme === "light") {
|
|
||||||
themePreference = "light"
|
|
||||||
themeDark = false
|
|
||||||
} else {
|
|
||||||
themePreference = "dark"
|
|
||||||
themeDark = true
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsDarkSignal(themeDark)
|
|
||||||
applyTheme(themeDark)
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("Failed to load theme from config:", error)
|
|
||||||
themePreference = "dark"
|
|
||||||
const themeDark = true
|
|
||||||
setIsDarkSignal(themeDark)
|
|
||||||
applyTheme(themeDark)
|
|
||||||
}
|
}
|
||||||
|
return preference === "dark"
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveTheme(dark: boolean) {
|
const applyResolvedTheme = () => {
|
||||||
try {
|
const dark = resolveDarkTheme()
|
||||||
const config = await storage.loadConfig()
|
setIsDarkSignal(dark)
|
||||||
const nextPreference = dark ? "dark" : "light"
|
applyTheme(dark)
|
||||||
config.theme = nextPreference
|
|
||||||
themePreference = nextPreference
|
|
||||||
await storage.saveConfig(config)
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("Failed to save theme to config:", error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
applyResolvedTheme()
|
||||||
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadTheme()
|
|
||||||
|
|
||||||
const unsubscribe = storage.onConfigChanged(() => {
|
|
||||||
loadTheme()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Listen for system theme changes
|
|
||||||
const handleSystemThemeChange = (event: MediaQueryListEvent) => {
|
const handleSystemThemeChange = (event: MediaQueryListEvent) => {
|
||||||
if (themePreference === "system") {
|
if (themePreference() === "system") {
|
||||||
setIsDarkSignal(event.matches)
|
setIsDarkSignal(event.matches)
|
||||||
applyTheme(event.matches)
|
applyTheme(event.matches)
|
||||||
}
|
}
|
||||||
@@ -86,19 +52,12 @@ export function ThemeProvider(props: { children: JSX.Element }) {
|
|||||||
systemPrefersDark.addEventListener("change", handleSystemThemeChange)
|
systemPrefersDark.addEventListener("change", handleSystemThemeChange)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe()
|
|
||||||
systemPrefersDark.removeEventListener("change", handleSystemThemeChange)
|
systemPrefersDark.removeEventListener("change", handleSystemThemeChange)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
applyTheme(isDark())
|
|
||||||
})
|
|
||||||
|
|
||||||
const setTheme = (dark: boolean) => {
|
const setTheme = (dark: boolean) => {
|
||||||
setIsDarkSignal(dark)
|
setThemePreference(dark ? "dark" : "light")
|
||||||
applyTheme(dark)
|
|
||||||
saveTheme(dark)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
clearInstanceDraftPrompts,
|
clearInstanceDraftPrompts,
|
||||||
} from "./sessions"
|
} from "./sessions"
|
||||||
import { fetchCommands, clearCommands } from "./commands"
|
import { fetchCommands, clearCommands } from "./commands"
|
||||||
import { preferences, updateLastUsedBinary } from "./preferences"
|
import { preferences } from "./preferences"
|
||||||
import { computeDisplayParts } from "./session-messages"
|
import { computeDisplayParts } from "./session-messages"
|
||||||
import { withSession, setSessionPendingPermission } from "./session-state"
|
import { withSession, setSessionPendingPermission } from "./session-state"
|
||||||
import { setHasInstances } from "./ui"
|
import { setHasInstances } from "./ui"
|
||||||
@@ -294,11 +294,7 @@ function removeInstance(id: string) {
|
|||||||
clearInstanceDraftPrompts(id)
|
clearInstanceDraftPrompts(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createInstance(folder: string, binaryPath?: string): Promise<string> {
|
async function createInstance(folder: string, _binaryPath?: string): Promise<string> {
|
||||||
if (binaryPath) {
|
|
||||||
updateLastUsedBinary(binaryPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const workspace = await cliApi.createWorkspace({ path: folder })
|
const workspace = await cliApi.createWorkspace({ path: folder })
|
||||||
upsertWorkspace(workspace)
|
upsertWorkspace(workspace)
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import { createContext, createSignal, onMount, useContext } from "solid-js"
|
import { createContext, createMemo, createSignal, onMount, useContext } from "solid-js"
|
||||||
import type { Accessor, ParentComponent } from "solid-js"
|
import type { Accessor, ParentComponent } from "solid-js"
|
||||||
import { storage, type ConfigData } from "../lib/storage"
|
import { storage, type ConfigData } from "../lib/storage"
|
||||||
|
|
||||||
|
type DeepReadonly<T> = T extends (...args: any[]) => unknown
|
||||||
|
? T
|
||||||
|
: T extends Array<infer U>
|
||||||
|
? ReadonlyArray<DeepReadonly<U>>
|
||||||
|
: T extends object
|
||||||
|
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
|
||||||
|
: T
|
||||||
|
|
||||||
export interface ModelPreference {
|
export interface ModelPreference {
|
||||||
providerId: string
|
providerId: string
|
||||||
modelId: string
|
modelId: string
|
||||||
@@ -36,6 +44,8 @@ export interface RecentFolder {
|
|||||||
lastAccessed: number
|
lastAccessed: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ThemePreference = NonNullable<ConfigData["theme"]>
|
||||||
|
|
||||||
const MAX_RECENT_FOLDERS = 20
|
const MAX_RECENT_FOLDERS = 20
|
||||||
const MAX_RECENT_MODELS = 5
|
const MAX_RECENT_MODELS = 5
|
||||||
|
|
||||||
@@ -49,6 +59,18 @@ const defaultPreferences: Preferences = {
|
|||||||
diagnosticsExpansion: "expanded",
|
diagnosticsExpansion: "expanded",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deepEqual(a: unknown, b: unknown): boolean {
|
||||||
|
if (a === b) return true
|
||||||
|
if (typeof a === "object" && a !== null && typeof b === "object" && b !== null) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(a) === JSON.stringify(b)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to compare preference values", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
function normalizePreferences(pref?: Partial<Preferences>): Preferences {
|
function normalizePreferences(pref?: Partial<Preferences>): Preferences {
|
||||||
const environmentVariables = {
|
const environmentVariables = {
|
||||||
...defaultPreferences.environmentVariables,
|
...defaultPreferences.environmentVariables,
|
||||||
@@ -78,73 +100,148 @@ function normalizePreferences(pref?: Partial<Preferences>): Preferences {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [preferences, setPreferences] = createSignal<Preferences>(normalizePreferences())
|
const [internalConfig, setInternalConfig] = createSignal<ConfigData>(buildFallbackConfig())
|
||||||
const [recentFolders, setRecentFolders] = createSignal<RecentFolder[]>([])
|
const config = createMemo<DeepReadonly<ConfigData>>(() => internalConfig())
|
||||||
const [opencodeBinaries, setOpenCodeBinaries] = createSignal<OpenCodeBinary[]>([])
|
|
||||||
const [isConfigLoaded, setIsConfigLoaded] = createSignal(false)
|
const [isConfigLoaded, setIsConfigLoaded] = createSignal(false)
|
||||||
let cachedConfig: ConfigData = {
|
const preferences = createMemo<Preferences>(() => internalConfig().preferences)
|
||||||
preferences: normalizePreferences(),
|
const recentFolders = createMemo<RecentFolder[]>(() => internalConfig().recentFolders ?? [])
|
||||||
recentFolders: [],
|
const opencodeBinaries = createMemo<OpenCodeBinary[]>(() => internalConfig().opencodeBinaries ?? [])
|
||||||
opencodeBinaries: [],
|
const themePreference = createMemo<ThemePreference>(() => internalConfig().theme ?? "dark")
|
||||||
}
|
|
||||||
let loadPromise: Promise<void> | null = null
|
let loadPromise: Promise<void> | null = null
|
||||||
|
|
||||||
async function loadConfig(): Promise<void> {
|
function normalizeConfig(config?: ConfigData | null): ConfigData {
|
||||||
|
return {
|
||||||
|
preferences: normalizePreferences(config?.preferences),
|
||||||
|
recentFolders: (config?.recentFolders ?? []).map((folder) => ({ ...folder })),
|
||||||
|
opencodeBinaries: (config?.opencodeBinaries ?? []).map((binary) => ({ ...binary })),
|
||||||
|
theme: config?.theme ?? "dark",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFallbackConfig(): ConfigData {
|
||||||
|
return normalizeConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncConfig(source?: ConfigData): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const config = await storage.loadConfig()
|
const configData = source ?? (await storage.loadConfig())
|
||||||
cachedConfig = {
|
applyConfig(configData)
|
||||||
...config,
|
|
||||||
preferences: normalizePreferences(config.preferences),
|
|
||||||
recentFolders: config.recentFolders ?? [],
|
|
||||||
opencodeBinaries: config.opencodeBinaries ?? [],
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load config:", error)
|
console.error("Failed to load config:", error)
|
||||||
cachedConfig = {
|
applyConfig(buildFallbackConfig())
|
||||||
...cachedConfig,
|
|
||||||
preferences: normalizePreferences(),
|
|
||||||
recentFolders: [],
|
|
||||||
opencodeBinaries: [],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setPreferences(cachedConfig.preferences)
|
function applyConfig(next: ConfigData) {
|
||||||
setRecentFolders(cachedConfig.recentFolders)
|
setInternalConfig(normalizeConfig(next))
|
||||||
setOpenCodeBinaries(cachedConfig.opencodeBinaries)
|
|
||||||
setIsConfigLoaded(true)
|
setIsConfigLoaded(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveConfig(): Promise<void> {
|
function cloneConfigForUpdate(): ConfigData {
|
||||||
|
return normalizeConfig(internalConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
function logConfigDiff(previous: ConfigData, next: ConfigData) {
|
||||||
|
if (deepEqual(previous, next)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const changes = diffObjects(previous, next)
|
||||||
|
if (changes.length > 0) {
|
||||||
|
console.debug("[Config] Changes", changes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function diffObjects(previous: unknown, next: unknown, path: string[] = []): string[] {
|
||||||
|
if (previous === next) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof previous !== "object" || previous === null || typeof next !== "object" || next === null) {
|
||||||
|
return [path.join(".")]
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevKeys = Object.keys(previous as Record<string, unknown>)
|
||||||
|
const nextKeys = Object.keys(next as Record<string, unknown>)
|
||||||
|
const allKeys = new Set([...prevKeys, ...nextKeys])
|
||||||
|
const changes: string[] = []
|
||||||
|
|
||||||
|
for (const key of allKeys) {
|
||||||
|
const childPath = [...path, key]
|
||||||
|
const prevValue = (previous as Record<string, unknown>)[key]
|
||||||
|
const nextValue = (next as Record<string, unknown>)[key]
|
||||||
|
changes.push(...diffObjects(prevValue, nextValue, childPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateConfig(mutator: (draft: ConfigData) => void): void {
|
||||||
|
const previous = internalConfig()
|
||||||
|
const draft = cloneConfigForUpdate()
|
||||||
|
mutator(draft)
|
||||||
|
logConfigDiff(previous, draft)
|
||||||
|
applyConfig(draft)
|
||||||
|
void persistFullConfig(draft)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistFullConfig(next: ConfigData): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await ensureConfigLoaded()
|
await ensureConfigLoaded()
|
||||||
const config: ConfigData = {
|
await storage.updateConfig(next)
|
||||||
...cachedConfig,
|
|
||||||
preferences: preferences(),
|
|
||||||
recentFolders: recentFolders(),
|
|
||||||
opencodeBinaries: opencodeBinaries(),
|
|
||||||
}
|
|
||||||
cachedConfig = config
|
|
||||||
await storage.saveConfig(config)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save config:", error)
|
console.error("Failed to save config:", error)
|
||||||
|
void syncConfig().catch((syncError: unknown) => {
|
||||||
|
console.error("Failed to refresh config:", syncError)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setThemePreference(preference: ThemePreference): void {
|
||||||
|
if (themePreference() === preference) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateConfig((draft) => {
|
||||||
|
draft.theme = preference
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureConfigLoaded(): Promise<void> {
|
async function ensureConfigLoaded(): Promise<void> {
|
||||||
if (isConfigLoaded()) return
|
if (isConfigLoaded()) return
|
||||||
if (!loadPromise) {
|
if (!loadPromise) {
|
||||||
loadPromise = loadConfig().finally(() => {
|
loadPromise = syncConfig().finally(() => {
|
||||||
loadPromise = null
|
loadPromise = null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
await loadPromise
|
await loadPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildRecentFolderList(path: string, source: RecentFolder[]): RecentFolder[] {
|
||||||
|
const folders = source.filter((f) => f.path !== path)
|
||||||
|
folders.unshift({ path, lastAccessed: Date.now() })
|
||||||
|
return folders.slice(0, MAX_RECENT_FOLDERS)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBinaryList(path: string, version: string | undefined, source: OpenCodeBinary[]): OpenCodeBinary[] {
|
||||||
|
const timestamp = Date.now()
|
||||||
|
const existing = source.find((b) => b.path === path)
|
||||||
|
if (existing) {
|
||||||
|
const updatedEntry: OpenCodeBinary = { ...existing, lastUsed: timestamp }
|
||||||
|
const remaining = source.filter((b) => b.path !== path)
|
||||||
|
return [updatedEntry, ...remaining]
|
||||||
|
}
|
||||||
|
const nextEntry: OpenCodeBinary = version ? { path, version, lastUsed: timestamp } : { path, lastUsed: timestamp }
|
||||||
|
return [nextEntry, ...source].slice(0, 10)
|
||||||
|
}
|
||||||
|
|
||||||
function updatePreferences(updates: Partial<Preferences>): void {
|
function updatePreferences(updates: Partial<Preferences>): void {
|
||||||
const updated = normalizePreferences({ ...preferences(), ...updates })
|
const current = internalConfig().preferences
|
||||||
setPreferences(updated)
|
const merged = normalizePreferences({ ...current, ...updates })
|
||||||
saveConfig().catch(console.error)
|
if (deepEqual(current, merged)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateConfig((draft) => {
|
||||||
|
draft.preferences = merged
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDiffViewMode(mode: DiffViewMode): void {
|
function setDiffViewMode(mode: DiffViewMode): void {
|
||||||
@@ -167,54 +264,44 @@ function toggleShowThinkingBlocks(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addRecentFolder(path: string): void {
|
function addRecentFolder(path: string): void {
|
||||||
const folders = recentFolders().filter((f) => f.path !== path)
|
updateConfig((draft) => {
|
||||||
folders.unshift({ path, lastAccessed: Date.now() })
|
draft.recentFolders = buildRecentFolderList(path, draft.recentFolders)
|
||||||
|
})
|
||||||
const trimmed = folders.slice(0, MAX_RECENT_FOLDERS)
|
|
||||||
setRecentFolders(trimmed)
|
|
||||||
saveConfig().catch(console.error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeRecentFolder(path: string): void {
|
function removeRecentFolder(path: string): void {
|
||||||
const folders = recentFolders().filter((f) => f.path !== path)
|
updateConfig((draft) => {
|
||||||
setRecentFolders(folders)
|
draft.recentFolders = draft.recentFolders.filter((f) => f.path !== path)
|
||||||
saveConfig().catch(console.error)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function addOpenCodeBinary(path: string, version?: string): void {
|
function addOpenCodeBinary(path: string, version?: string): void {
|
||||||
const binaries = opencodeBinaries().filter((b) => b.path !== path)
|
updateConfig((draft) => {
|
||||||
const lastUsed = Date.now()
|
draft.opencodeBinaries = buildBinaryList(path, version, draft.opencodeBinaries)
|
||||||
const binaryEntry: OpenCodeBinary = version ? { path, version, lastUsed } : { path, lastUsed }
|
})
|
||||||
binaries.unshift(binaryEntry)
|
|
||||||
|
|
||||||
const trimmed = binaries.slice(0, 10) // Keep max 10 binaries
|
|
||||||
setOpenCodeBinaries(trimmed)
|
|
||||||
saveConfig().catch(console.error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeOpenCodeBinary(path: string): void {
|
function removeOpenCodeBinary(path: string): void {
|
||||||
const binaries = opencodeBinaries().filter((b) => b.path !== path)
|
updateConfig((draft) => {
|
||||||
setOpenCodeBinaries(binaries)
|
draft.opencodeBinaries = draft.opencodeBinaries.filter((b) => b.path !== path)
|
||||||
saveConfig().catch(console.error)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLastUsedBinary(path: string): void {
|
function updateLastUsedBinary(path: string): void {
|
||||||
updatePreferences({ lastUsedBinary: path })
|
const target = path || preferences().lastUsedBinary || "opencode"
|
||||||
|
updateConfig((draft) => {
|
||||||
|
draft.preferences = normalizePreferences({ ...draft.preferences, lastUsedBinary: target })
|
||||||
|
draft.opencodeBinaries = buildBinaryList(target, undefined, draft.opencodeBinaries)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const binaries = opencodeBinaries()
|
function recordWorkspaceLaunch(folderPath: string, binaryPath?: string): void {
|
||||||
let binary = binaries.find((b) => b.path === path)
|
updateConfig((draft) => {
|
||||||
|
const targetBinary = binaryPath && binaryPath.trim().length > 0 ? binaryPath : draft.preferences.lastUsedBinary || "opencode"
|
||||||
// If binary not found in list, add it (for system PATH "opencode")
|
draft.recentFolders = buildRecentFolderList(folderPath, draft.recentFolders)
|
||||||
if (!binary) {
|
draft.preferences = normalizePreferences({ ...draft.preferences, lastUsedBinary: targetBinary })
|
||||||
addOpenCodeBinary(path)
|
draft.opencodeBinaries = buildBinaryList(targetBinary, undefined, draft.opencodeBinaries)
|
||||||
binary = { path, lastUsed: Date.now() }
|
})
|
||||||
} else {
|
|
||||||
binary.lastUsed = Date.now()
|
|
||||||
// Move to front
|
|
||||||
const sorted = [binary, ...binaries.filter((b) => b.path !== path)]
|
|
||||||
setOpenCodeBinaries(sorted)
|
|
||||||
saveConfig().catch(console.error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateEnvironmentVariables(envVars: Record<string, string>): void {
|
function updateEnvironmentVariables(envVars: Record<string, string>): void {
|
||||||
@@ -264,15 +351,19 @@ function getAgentModelPreference(instanceId: string, agent: string): ModelPrefer
|
|||||||
return preferences().agentModelSelections?.[instanceId]?.[agent]
|
return preferences().agentModelSelections?.[instanceId]?.[agent]
|
||||||
}
|
}
|
||||||
|
|
||||||
void ensureConfigLoaded().catch((error) => {
|
void ensureConfigLoaded().catch((error: unknown) => {
|
||||||
console.error("Failed to initialize config:", error)
|
console.error("Failed to initialize config:", error)
|
||||||
})
|
})
|
||||||
|
|
||||||
interface ConfigContextValue {
|
interface ConfigContextValue {
|
||||||
isLoaded: Accessor<boolean>
|
isLoaded: Accessor<boolean>
|
||||||
|
config: typeof config
|
||||||
preferences: typeof preferences
|
preferences: typeof preferences
|
||||||
recentFolders: typeof recentFolders
|
recentFolders: typeof recentFolders
|
||||||
opencodeBinaries: typeof opencodeBinaries
|
opencodeBinaries: typeof opencodeBinaries
|
||||||
|
themePreference: typeof themePreference
|
||||||
|
setThemePreference: typeof setThemePreference
|
||||||
|
updateConfig: typeof updateConfig
|
||||||
toggleShowThinkingBlocks: typeof toggleShowThinkingBlocks
|
toggleShowThinkingBlocks: typeof toggleShowThinkingBlocks
|
||||||
setDiffViewMode: typeof setDiffViewMode
|
setDiffViewMode: typeof setDiffViewMode
|
||||||
setToolOutputExpansion: typeof setToolOutputExpansion
|
setToolOutputExpansion: typeof setToolOutputExpansion
|
||||||
@@ -282,6 +373,7 @@ interface ConfigContextValue {
|
|||||||
addOpenCodeBinary: typeof addOpenCodeBinary
|
addOpenCodeBinary: typeof addOpenCodeBinary
|
||||||
removeOpenCodeBinary: typeof removeOpenCodeBinary
|
removeOpenCodeBinary: typeof removeOpenCodeBinary
|
||||||
updateLastUsedBinary: typeof updateLastUsedBinary
|
updateLastUsedBinary: typeof updateLastUsedBinary
|
||||||
|
recordWorkspaceLaunch: typeof recordWorkspaceLaunch
|
||||||
updatePreferences: typeof updatePreferences
|
updatePreferences: typeof updatePreferences
|
||||||
updateEnvironmentVariables: typeof updateEnvironmentVariables
|
updateEnvironmentVariables: typeof updateEnvironmentVariables
|
||||||
addEnvironmentVariable: typeof addEnvironmentVariable
|
addEnvironmentVariable: typeof addEnvironmentVariable
|
||||||
@@ -295,9 +387,13 @@ const ConfigContext = createContext<ConfigContextValue>()
|
|||||||
|
|
||||||
const configContextValue: ConfigContextValue = {
|
const configContextValue: ConfigContextValue = {
|
||||||
isLoaded: isConfigLoaded,
|
isLoaded: isConfigLoaded,
|
||||||
|
config,
|
||||||
preferences,
|
preferences,
|
||||||
recentFolders,
|
recentFolders,
|
||||||
opencodeBinaries,
|
opencodeBinaries,
|
||||||
|
themePreference,
|
||||||
|
setThemePreference,
|
||||||
|
updateConfig,
|
||||||
toggleShowThinkingBlocks,
|
toggleShowThinkingBlocks,
|
||||||
setDiffViewMode,
|
setDiffViewMode,
|
||||||
setToolOutputExpansion,
|
setToolOutputExpansion,
|
||||||
@@ -307,6 +403,7 @@ const configContextValue: ConfigContextValue = {
|
|||||||
addOpenCodeBinary,
|
addOpenCodeBinary,
|
||||||
removeOpenCodeBinary,
|
removeOpenCodeBinary,
|
||||||
updateLastUsedBinary,
|
updateLastUsedBinary,
|
||||||
|
recordWorkspaceLaunch,
|
||||||
updatePreferences,
|
updatePreferences,
|
||||||
updateEnvironmentVariables,
|
updateEnvironmentVariables,
|
||||||
addEnvironmentVariable,
|
addEnvironmentVariable,
|
||||||
@@ -318,12 +415,12 @@ const configContextValue: ConfigContextValue = {
|
|||||||
|
|
||||||
const ConfigProvider: ParentComponent = (props) => {
|
const ConfigProvider: ParentComponent = (props) => {
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
ensureConfigLoaded().catch((error) => {
|
ensureConfigLoaded().catch((error: unknown) => {
|
||||||
console.error("Failed to initialize config:", error)
|
console.error("Failed to initialize config:", error)
|
||||||
})
|
})
|
||||||
|
|
||||||
const unsubscribe = storage.onConfigChanged(() => {
|
const unsubscribe = storage.onConfigChanged((config) => {
|
||||||
loadConfig().catch((error) => {
|
syncConfig(config).catch((error: unknown) => {
|
||||||
console.error("Failed to refresh config:", error)
|
console.error("Failed to refresh config:", error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -347,7 +444,9 @@ function useConfig(): ConfigContextValue {
|
|||||||
export {
|
export {
|
||||||
ConfigProvider,
|
ConfigProvider,
|
||||||
useConfig,
|
useConfig,
|
||||||
|
config,
|
||||||
preferences,
|
preferences,
|
||||||
|
updateConfig,
|
||||||
updatePreferences,
|
updatePreferences,
|
||||||
toggleShowThinkingBlocks,
|
toggleShowThinkingBlocks,
|
||||||
recentFolders,
|
recentFolders,
|
||||||
@@ -366,4 +465,7 @@ export {
|
|||||||
setDiffViewMode,
|
setDiffViewMode,
|
||||||
setToolOutputExpansion,
|
setToolOutputExpansion,
|
||||||
setDiagnosticsExpansion,
|
setDiagnosticsExpansion,
|
||||||
|
themePreference,
|
||||||
|
setThemePreference,
|
||||||
|
recordWorkspaceLaunch,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user