From 1c12f6f7547d0040f15c70876f808f9e4115d939 Mon Sep 17 00:00:00 2001 From: Brace Sproul Date: Mon, 9 Jun 2025 16:41:52 -0700 Subject: [PATCH] feat: Hookup config store (#99) * feat: Hookup config store * cr --- .../configuration-sidebar/config-field.tsx | 11 ++-- .../configuration-sidebar/index.tsx | 50 +++++++++++---- apps/web/src/components/thread/index.tsx | 8 +-- .../web/src/components/thread/messages/ai.tsx | 2 - apps/web/src/hooks/use-config-store.tsx | 62 ++++--------------- 5 files changed, 63 insertions(+), 70 deletions(-) diff --git a/apps/web/src/components/configuration-sidebar/config-field.tsx b/apps/web/src/components/configuration-sidebar/config-field.tsx index 2c78968d..5fd04dae 100644 --- a/apps/web/src/components/configuration-sidebar/config-field.tsx +++ b/apps/web/src/components/configuration-sidebar/config-field.tsx @@ -13,12 +13,13 @@ import { SelectValue, } from "@/components/ui/select"; import { Label } from "@/components/ui/label"; -import { useConfigStore } from "@/hooks/use-config-store"; +import { DEFAULT_CONFIG_KEY, useConfigStore } from "@/hooks/use-config-store"; import { AlertCircle } from "lucide-react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import _ from "lodash"; import { cn } from "@/lib/utils"; +import { useQueryState } from "nuqs"; interface Option { label: string; @@ -62,6 +63,8 @@ export function ConfigField({ value: externalValue, // TODO: Rename to avoid conflict setValue: externalSetValue, // TODO: Rename to avoid conflict }: ConfigFieldProps) { + const [threadId] = useQueryState("threadId"); + const configKey = threadId || DEFAULT_CONFIG_KEY; const store = useConfigStore(); const [jsonError, setJsonError] = useState(null); @@ -70,14 +73,14 @@ export function ConfigField({ const currentValue = isExternallyManaged ? externalValue - : store.configs[id]?.[id]; + : store.configs[configKey]?.[id]; const handleChange = (newValue: any) => { setJsonError(null); if (isExternallyManaged && externalSetValue) { externalSetValue(newValue); } else { - store.updateConfig(id, newValue); + store.updateConfig(configKey, id, newValue); } }; @@ -97,7 +100,7 @@ export function ConfigField({ if (isExternallyManaged && externalSetValue) { externalSetValue(jsonString); } else { - store.updateConfig(id, jsonString); + store.updateConfig(configKey, id, jsonString); } setJsonError("Invalid JSON format"); } diff --git a/apps/web/src/components/configuration-sidebar/index.tsx b/apps/web/src/components/configuration-sidebar/index.tsx index 5057cd63..a4c20729 100644 --- a/apps/web/src/components/configuration-sidebar/index.tsx +++ b/apps/web/src/components/configuration-sidebar/index.tsx @@ -4,7 +4,7 @@ import { forwardRef, ForwardedRef, useState, useEffect } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ConfigField } from "@/components/configuration-sidebar/config-field"; import { ConfigSection } from "@/components/configuration-sidebar/config-section"; -import { useConfigStore } from "@/hooks/use-config-store"; +import { useConfigStore, DEFAULT_CONFIG_KEY } from "@/hooks/use-config-store"; import { ScrollArea } from "@/components/ui/scroll-area"; import { cn } from "@/lib/utils"; import { Skeleton } from "@/components/ui/skeleton"; @@ -12,11 +12,15 @@ import type { ConfigurableFieldUIMetadata } from "@open-swe/shared/configurable- import { Button } from "@/components/ui/button"; import { PanelRightOpen } from "lucide-react"; import { GraphConfigurationMetadata } from "@open-swe/shared/open-swe/types"; +import { useQueryState } from "nuqs"; +import { useStreamContext } from "@/providers/Stream"; /** * Extract configuration metadata from the GraphConfiguration Zod schema */ -function extractConfigurationsFromSchema(): ConfigurableFieldUIMetadata[] { +function extractConfigurationsFromSchema( + configurable: Record, +): ConfigurableFieldUIMetadata[] { const configurations: ConfigurableFieldUIMetadata[] = []; for (const [label, { x_open_swe_ui_config: metadata }] of Object.entries( @@ -28,7 +32,7 @@ function extractConfigurationsFromSchema(): ConfigurableFieldUIMetadata[] { configurations.push({ label, type: metadata.type, - default: metadata.default, + default: configurable[label] || metadata.default, description: metadata.description, placeholder: metadata.placeholder, options: metadata.options, @@ -52,6 +56,9 @@ export const ConfigurationSidebar = forwardRef< AIConfigPanelProps >(({ className, open, onClose }, ref: ForwardedRef) => { const { configs, updateConfig } = useConfigStore(); + const stream = useStreamContext(); + + const [threadId] = useQueryState("threadId"); const [configurations, setConfigurations] = useState< ConfigurableFieldUIMetadata[] @@ -60,17 +67,38 @@ export const ConfigurationSidebar = forwardRef< useEffect(() => { setLoading(true); + const configKey = threadId || DEFAULT_CONFIG_KEY; - const actualConfigs = extractConfigurationsFromSchema(); - actualConfigs.forEach((config) => { - if (configs[config.label] === undefined && config.default !== undefined) { - updateConfig(config.label, config.default); - } - }); + if (threadId) { + stream.client.threads.get(threadId).then((t) => { + if ( + !("config" in t) || + !(t as any).config || + !(t as any).config.configurable + ) { + console.error("Thread does not have config key", t); + return; + } - setConfigurations(actualConfigs); + const actualConfigs = extractConfigurationsFromSchema( + (t.config as any).configurable, + ); + actualConfigs.forEach((c) => { + // Always update the config store with either the default values, or the values from the thread. + updateConfig(configKey, c.label, c.default); + }); + + setConfigurations(actualConfigs); + }); + } else { + const actualConfigs = extractConfigurationsFromSchema({}); + actualConfigs.forEach((c) => { + updateConfig(configKey, c.label, c.default); + }); + setConfigurations(actualConfigs); + } setLoading(false); - }, [configs, updateConfig]); + }, [threadId]); return (
diff --git a/apps/web/src/hooks/use-config-store.tsx b/apps/web/src/hooks/use-config-store.tsx index 125c06ef..0f4a9a91 100644 --- a/apps/web/src/hooks/use-config-store.tsx +++ b/apps/web/src/hooks/use-config-store.tsx @@ -2,21 +2,16 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; -import type { ConfigurableFieldUIMetadata } from "@open-swe/shared/configurable-metadata"; export const DEFAULT_CONFIG_KEY = "open_swe_default_config"; interface ConfigState { configs: Record; - getConfig: (key: string) => Record; + getConfig: (namespace: string) => Record; getConfigs: () => Record; - updateConfig: (key: string, value: any) => void; - resetConfig: (key: string) => void; - setDefaultConfig: ( - key: string, - configurations: ConfigurableFieldUIMetadata[], - ) => void; - resetStore: (key: string) => void; + updateConfig: (namespace: string, key: string, value: any) => void; + resetConfig: (namespace: string) => void; + resetStore: (namespace: string) => void; } export const useConfigStore = create()( @@ -24,9 +19,9 @@ export const useConfigStore = create()( (set, get) => ({ configs: {}, - getConfig: (key: string) => { + getConfig: (namespace: string) => { const state = get(); - const baseConfig = state.configs[key]; + const baseConfig = state.configs[namespace]; const configObj = { ...baseConfig, }; @@ -36,33 +31,23 @@ export const useConfigStore = create()( getConfigs: () => { const state = get(); - const flatConfigs: Record = {}; - Object.entries(state.configs).forEach(([key, configObj]) => { - if ( - configObj && - typeof configObj === "object" && - configObj[key] !== undefined - ) { - flatConfigs[key] = configObj[key]; - } - }); - return flatConfigs; + return state.configs; }, - updateConfig: (key, value) => + updateConfig: (namespace: string, key: string, value: any) => set((state) => ({ configs: { ...state.configs, - [key]: { - ...(state.configs[key] || {}), + [namespace]: { + ...(state.configs[namespace] || {}), [key]: value, }, }, })), - resetConfig: (key: string) => { + resetConfig: (namespace: string) => { set((state) => { - const config = state.configs[key]; + const config = state.configs[namespace]; if (!config || !config.__defaultValues) { return state; } @@ -70,33 +55,12 @@ export const useConfigStore = create()( return { configs: { ...state.configs, - [key]: defaultsToUse, + [namespace]: defaultsToUse, }, }; }); }, - setDefaultConfig: ( - key: string, - configurations: ConfigurableFieldUIMetadata[], - ) => { - const defaultConfig: Record = {}; - configurations.forEach((config: ConfigurableFieldUIMetadata) => { - if (config.default !== undefined) { - defaultConfig[config.label] = config.default; - } - }); - - defaultConfig.__defaultValues = { ...defaultConfig }; - - set((currentState) => ({ - configs: { - ...currentState.configs, - [key]: defaultConfig, - }, - })); - }, - resetStore: () => set({ configs: {} }), }), {