diff --git a/src/ui/components/settings/sections/BudgetSection.tsx b/src/ui/components/settings/sections/BudgetSection.tsx index 7b329f1..f645942 100644 --- a/src/ui/components/settings/sections/BudgetSection.tsx +++ b/src/ui/components/settings/sections/BudgetSection.tsx @@ -1,3 +1,4 @@ +import { useState, useEffect } from "react"; import { Switch } from "@ui/components/ui/switch"; import { Slider } from "@ui/components/ui/slider"; import { SettingsRow } from "../SettingsRow"; @@ -5,14 +6,25 @@ import { useGlobalSettings, useUpdateGlobalSetting, } from "@core/api/useSettings"; +import { undoToast } from "@ui/lib/toast"; export function BudgetSection() { const { data: settings } = useGlobalSettings(); const { mutate: updateSetting } = useUpdateGlobalSetting(); - if (!settings) return null; + // Local slider state — only persisted on mouse-up / touch-end + const [localCeiling, setLocalCeiling] = useState( + undefined, + ); + + // Sync from server when settings load or change externally + useEffect(() => { + if (settings) setLocalCeiling(settings.budgetCeilingPercent); + }, [settings]); + + if (!settings || localCeiling === undefined) return null; - const ceiling = settings.budgetCeilingPercent; + const ceiling = localCeiling; const reserved = 100 - ceiling; return ( @@ -44,11 +56,26 @@ export function BudgetSection() { + setLocalCeiling(value) + } + onValueCommit={([value]) => { + const prev = settings.budgetCeilingPercent; + if (prev === value) return; updateSetting({ key: "budgetCeilingPercent", value, - }) - } + }); + undoToast( + `Budget ceiling → ${value}%`, + () => { + setLocalCeiling(prev); + updateSetting({ + key: "budgetCeilingPercent", + value: prev, + }); + }, + ); + }} min={10} max={100} step={5} diff --git a/src/ui/components/settings/sections/ProjectSection.tsx b/src/ui/components/settings/sections/ProjectSection.tsx index a68b194..ac95230 100644 --- a/src/ui/components/settings/sections/ProjectSection.tsx +++ b/src/ui/components/settings/sections/ProjectSection.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, useCallback } from "react"; +import { useState, useEffect, useRef, useCallback, useMemo } from "react"; import { useRepositories, useGitBranches } from "@core/api/useRepositories"; import { useGlobalSettings, @@ -19,6 +19,7 @@ import { Slider } from "@ui/components/ui/slider"; import type { BranchPrefixMode } from "@core/types/settings"; import type { ScheduleMode } from "@core/types/agent"; import { Trash2, Clock, Zap, Hand } from "lucide-react"; +import { undoToast } from "@ui/lib/toast"; interface ProjectSectionProps { repositoryId: string; @@ -53,6 +54,22 @@ export function ProjectSection({ const agentPrefsDirty = useRef(false); const scanPrefsDirty = useRef(false); + // Local slider state for project budget — only persisted on mouse-up + const [localBudget, setLocalBudget] = useState( + undefined, + ); + const serverBudget = useMemo( + () => + overrides && globalSettings + ? (overrides.overrideBudgetCeilingPercent ?? + globalSettings.budgetCeilingPercent) + : undefined, + [overrides, globalSettings], + ); + useEffect(() => { + if (serverBudget !== undefined) setLocalBudget(serverBudget); + }, [serverBudget]); + const repo = repositories?.find((r) => r.id === repositoryId); const { data: branches } = useGitBranches(repo?.path); @@ -124,6 +141,7 @@ export function ProjectSection({ const effectiveSchedule = agentConfig?.scheduleMode ?? (globalSettings.agentMode as ScheduleMode); const effectiveBudget = + localBudget ?? overrides.overrideBudgetCeilingPercent ?? globalSettings.budgetCeilingPercent; @@ -421,13 +439,23 @@ export function ProjectSection({ + + ), + { id, duration: UNDO_DURATION }, + ); +} export function savedToast() { + // Don't show "Saved" while an undo toast is active — the undo toast + // already communicates that the change was applied. + if (undoActive) return; + toast.custom( () => (