fix(ui): re-sync settings/thread forms on loaded record

This commit is contained in:
Gabriel Brown
2026-07-11 17:14:06 -04:00
parent bca86196fb
commit a3ec04ad99
3 changed files with 216 additions and 2 deletions
@@ -1,6 +1,6 @@
'use client';
import { useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useMutation, useQuery } from 'convex/react';
import { Bot } from 'lucide-react';
import { toast } from 'sonner';
@@ -33,6 +33,7 @@ const runtimeLabels: Record<RuntimeName, string> = {
};
type AgentSettings = {
_id?: Id<'spoonAgentSettings'>;
enabled: boolean;
runtime?: RuntimeName | 'openai_direct';
defaultBaseBranch?: string;
@@ -127,6 +128,46 @@ export const SpoonAgentSettingsForm = ({
: settings.reasoningEffort,
);
// The parent gates rendering on a different query than `settings`, so this
// form can mount before the agent settings record resolves. Re-seed every
// field once the real record arrives (and again if the parent swaps to a
// different spoon) so a Save cannot persist the initial defaults over the
// user's saved configuration.
const hydratedSpoonId = useRef<Id<'spoons'> | null>(null);
useEffect(() => {
if (!settings || hydratedSpoonId.current === spoon._id) return;
const timeout = window.setTimeout(() => {
setEnabled(settings.enabled);
setDefaultBaseBranch(
settings.defaultBaseBranch ??
spoon.forkDefaultBranch ??
spoon.upstreamDefaultBranch,
);
setBranchPrefix(settings.branchPrefix);
setInstallCommand(settings.installCommand ?? '');
setCheckCommand(settings.checkCommand ?? '');
setTestCommand(settings.testCommand ?? '');
setEnvFilePath(settings.envFilePath ?? '.env.local');
setCustomEnvFilePath(settings.customEnvFilePath ?? '');
setMaterializeEnvFileByDefault(
settings.materializeEnvFileByDefault ?? false,
);
setAutoDetectCommands(settings.autoDetectCommands ?? true);
setAllowUserFileEditing(settings.allowUserFileEditing ?? true);
setAiProviderProfileId(settings.aiProviderProfileId ?? '__default');
setAgentModel(settings.aiProviderProfileId ? settings.agentModel : '');
setReasoningEffort(
!settings.aiProviderProfileId
? 'medium'
: settings.reasoningEffort === 'none'
? 'minimal'
: settings.reasoningEffort,
);
hydratedSpoonId.current = spoon._id;
}, 0);
return () => window.clearTimeout(timeout);
}, [settings, spoon]);
const supportedRuntimes = (selectedProfile?.supportedRuntimes ??
[]) as RuntimeName[];
const settingsRuntime = settings?.runtime as RuntimeName | undefined;
@@ -1,6 +1,6 @@
'use client';
import { useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useMutation, useQuery } from 'convex/react';
import { MessageSquarePlus } from 'lucide-react';
@@ -34,6 +34,7 @@ const runtimeLabels: Record<RuntimeName, string> = {
};
type AgentSettings = {
_id?: Id<'spoonAgentSettings'>;
defaultBaseBranch?: string;
runtime?: RuntimeName | 'openai_direct';
agentModel: string;
@@ -82,6 +83,33 @@ export const ThreadWorkspaceForm = ({
);
const [runtime, setRuntime] = useState<RuntimeName | undefined>();
const [submitting, setSubmitting] = useState(false);
// The parent gates rendering on a different query than `agentSettings`, so
// this form can mount before the agent settings record resolves. Re-seed the
// settings-derived fields once the real record arrives (and again if the
// parent swaps to a different spoon) so the form reflects saved config rather
// than the initial defaults.
const hydratedSpoonId = useRef<Id<'spoons'> | null>(null);
useEffect(() => {
if (!agentSettings || hydratedSpoonId.current === spoon._id) return;
const timeout = window.setTimeout(() => {
setBaseBranch(
agentSettings.defaultBaseBranch ??
spoon.forkDefaultBranch ??
spoon.upstreamDefaultBranch,
);
setMaterializeEnvFile(agentSettings.materializeEnvFileByDefault ?? false);
setEnvFilePath(
agentSettings.envFilePath === 'custom'
? (agentSettings.customEnvFilePath ?? '.env.local')
: (agentSettings.envFilePath ?? '.env.local'),
);
setAiProviderProfileId(agentSettings.aiProviderProfileId ?? '__settings');
hydratedSpoonId.current = spoon._id;
}, 0);
return () => window.clearTimeout(timeout);
}, [agentSettings, spoon]);
const effectiveProviderProfileId =
aiProviderProfileId === '__settings'
? (agentSettings?.aiProviderProfileId ?? defaultProfile?._id)