feat(web-evals): add multi-model launch and UI improvements (#9845)

Co-authored-by: Roo Code <roomote@roocode.com>
This commit is contained in:
Hannes Rudolph 2025-12-04 18:48:38 -07:00 committed by GitHub
parent 630c8bce90
commit c10d1d9ffc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -7,7 +7,7 @@ import { useQuery } from "@tanstack/react-query"
import { useForm, FormProvider } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { toast } from "sonner"
import { X, Rocket, Check, ChevronsUpDown, SlidersHorizontal, Info } from "lucide-react"
import { X, Rocket, Check, ChevronsUpDown, SlidersHorizontal, Info, Plus, Minus } from "lucide-react"
import {
globalSettingsSchema,
@ -16,7 +16,6 @@ import {
getModelId,
type ProviderSettings,
type GlobalSettings,
type ReasoningEffort,
} from "@roo-code/types"
import { createRun } from "@/actions/runs"
@ -44,7 +43,6 @@ import {
Button,
Checkbox,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
@ -66,11 +64,6 @@ import {
PopoverTrigger,
Slider,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Tooltip,
TooltipContent,
TooltipTrigger,
@ -84,21 +77,38 @@ type ImportedSettings = {
currentApiConfigName: string
}
// Type for a model selection entry
type ModelSelection = {
id: string
model: string
popoverOpen: boolean
}
// Type for a config selection entry (for import mode)
type ConfigSelection = {
id: string
configName: string
popoverOpen: boolean
}
export function NewRun() {
const router = useRouter()
const [provider, setModelSource] = useState<"roo" | "openrouter" | "other">("other")
const [modelPopoverOpen, setModelPopoverOpen] = useState(false)
const [useNativeToolProtocol, setUseNativeToolProtocol] = useState(true)
const [useMultipleNativeToolCalls, setUseMultipleNativeToolCalls] = useState(false)
const [reasoningEffort, setReasoningEffort] = useState<ReasoningEffort | "">("")
const [commandExecutionTimeout, setCommandExecutionTimeout] = useState(20)
const [terminalShellIntegrationTimeout, setTerminalShellIntegrationTimeout] = useState(30) // seconds
// State for imported settings with config selection
// State for multiple model selections
const [modelSelections, setModelSelections] = useState<ModelSelection[]>([
{ id: crypto.randomUUID(), model: "", popoverOpen: false },
])
// State for imported settings with multiple config selections
const [importedSettings, setImportedSettings] = useState<ImportedSettings | null>(null)
const [selectedConfigName, setSelectedConfigName] = useState<string>("")
const [configPopoverOpen, setConfigPopoverOpen] = useState(false)
const [configSelections, setConfigSelections] = useState<ConfigSelection[]>([
{ id: crypto.randomUUID(), configName: "", popoverOpen: false },
])
const openRouter = useOpenRouterModels()
const rooCodeCloud = useRooCodeCloudModels()
@ -134,7 +144,7 @@ export function NewRun() {
formState: { isSubmitting },
} = form
const [model, suite, settings] = watch(["model", "suite", "settings", "concurrency"])
const [suite, settings] = watch(["suite", "settings", "concurrency"])
// Load settings from localStorage on mount
useEffect(() => {
@ -250,6 +260,60 @@ export function NewRun() {
[getExercisesForLanguage, selectedExercises],
)
// Add a new model selection
const addModelSelection = useCallback(() => {
setModelSelections((prev) => [...prev, { id: crypto.randomUUID(), model: "", popoverOpen: false }])
}, [])
// Remove a model selection
const removeModelSelection = useCallback((id: string) => {
setModelSelections((prev) => prev.filter((s) => s.id !== id))
}, [])
// Update a model selection
const updateModelSelection = useCallback(
(id: string, model: string) => {
setModelSelections((prev) => prev.map((s) => (s.id === id ? { ...s, model, popoverOpen: false } : s)))
// Also set the form model field for validation (use first non-empty model)
setValue("model", model)
},
[setValue],
)
// Toggle popover for a model selection
const toggleModelPopover = useCallback((id: string, open: boolean) => {
setModelSelections((prev) => prev.map((s) => (s.id === id ? { ...s, popoverOpen: open } : s)))
}, [])
// Add a new config selection
const addConfigSelection = useCallback(() => {
setConfigSelections((prev) => [...prev, { id: crypto.randomUUID(), configName: "", popoverOpen: false }])
}, [])
// Remove a config selection
const removeConfigSelection = useCallback((id: string) => {
setConfigSelections((prev) => prev.filter((s) => s.id !== id))
}, [])
// Update a config selection
const updateConfigSelection = useCallback(
(id: string, configName: string) => {
setConfigSelections((prev) => prev.map((s) => (s.id === id ? { ...s, configName, popoverOpen: false } : s)))
// Also update the form settings for the first config (for validation)
if (importedSettings) {
const providerSettings = importedSettings.apiConfigs[configName] ?? {}
setValue("model", getModelId(providerSettings) ?? "")
setValue("settings", { ...EVALS_SETTINGS, ...providerSettings, ...importedSettings.globalSettings })
}
},
[importedSettings, setValue],
)
// Toggle popover for a config selection
const toggleConfigPopover = useCallback((id: string, open: boolean) => {
setConfigSelections((prev) => prev.map((s) => (s.id === id ? { ...s, popoverOpen: open } : s)))
}, [])
const onSubmit = useCallback(
async (values: CreateRun) => {
try {
@ -259,74 +323,104 @@ export function NewRun() {
return
}
// Build experiments settings
const experimentsSettings = useMultipleNativeToolCalls
? { experiments: { multipleNativeToolCalls: true } }
: {}
// Determine which selections to use based on provider
const selectionsToLaunch: { model: string; configName?: string }[] = []
if (provider === "openrouter") {
values.settings = {
...(values.settings || {}),
apiProvider: "openrouter",
openRouterModelId: model,
toolProtocol: useNativeToolProtocol ? "native" : "xml",
commandExecutionTimeout,
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout * 1000, // Convert to ms
...experimentsSettings,
if (provider === "other") {
// For import mode, use config selections
for (const config of configSelections) {
if (config.configName) {
selectionsToLaunch.push({ model: "", configName: config.configName })
}
}
} else if (provider === "roo") {
values.settings = {
...(values.settings || {}),
apiProvider: "roo",
apiModelId: model,
toolProtocol: useNativeToolProtocol ? "native" : "xml",
commandExecutionTimeout,
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout * 1000, // Convert to ms
...experimentsSettings,
...(reasoningEffort
? {
enableReasoningEffort: true,
reasoningEffort: reasoningEffort as ReasoningEffort,
}
: {}),
}
} else if (provider === "other" && values.settings) {
// For imported settings, merge in experiments and tool protocol
values.settings = {
...values.settings,
toolProtocol: useNativeToolProtocol ? "native" : "xml",
commandExecutionTimeout,
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout * 1000, // Convert to ms
...experimentsSettings,
} else {
// For openrouter/roo, use model selections
for (const selection of modelSelections) {
if (selection.model) {
selectionsToLaunch.push({ model: selection.model })
}
}
}
const { id } = await createRun(values)
router.push(`/runs/${id}`)
if (selectionsToLaunch.length === 0) {
toast.error("Please select at least one model or config")
return
}
// Show launching toast
const totalRuns = selectionsToLaunch.length
toast.info(totalRuns > 1 ? `Launching ${totalRuns} runs (every 20 seconds)...` : "Launching run...")
// Launch runs with 20-second delay between each
for (let i = 0; i < selectionsToLaunch.length; i++) {
const selection = selectionsToLaunch[i]!
// Wait 20 seconds between runs (except for the first one)
if (i > 0) {
await new Promise((resolve) => setTimeout(resolve, 20000))
}
const runValues = { ...values }
if (provider === "openrouter") {
runValues.model = selection.model
runValues.settings = {
...(runValues.settings || {}),
apiProvider: "openrouter",
openRouterModelId: selection.model,
toolProtocol: useNativeToolProtocol ? "native" : "xml",
commandExecutionTimeout,
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout * 1000,
}
} else if (provider === "roo") {
runValues.model = selection.model
runValues.settings = {
...(runValues.settings || {}),
apiProvider: "roo",
apiModelId: selection.model,
toolProtocol: useNativeToolProtocol ? "native" : "xml",
commandExecutionTimeout,
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout * 1000,
}
} else if (provider === "other" && selection.configName && importedSettings) {
const providerSettings = importedSettings.apiConfigs[selection.configName] ?? {}
runValues.model = getModelId(providerSettings) ?? ""
runValues.settings = {
...EVALS_SETTINGS,
...providerSettings,
...importedSettings.globalSettings,
toolProtocol: useNativeToolProtocol ? "native" : "xml",
commandExecutionTimeout,
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout * 1000,
}
}
try {
await createRun(runValues)
toast.success(`Run ${i + 1}/${totalRuns} launched`)
} catch (e) {
toast.error(`Run ${i + 1} failed: ${e instanceof Error ? e.message : "Unknown error"}`)
}
}
// Navigate back to main evals UI
router.push("/")
} catch (e) {
toast.error(e instanceof Error ? e.message : "An unknown error occurred.")
}
},
[
provider,
model,
modelSelections,
configSelections,
importedSettings,
router,
useNativeToolProtocol,
useMultipleNativeToolCalls,
reasoningEffort,
commandExecutionTimeout,
terminalShellIntegrationTimeout,
],
)
const onSelectModel = useCallback(
(model: string) => {
setValue("model", model)
setModelPopoverOpen(false)
},
[setValue, setModelPopoverOpen],
)
const onImportSettings = useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
@ -355,9 +449,9 @@ export function NewRun() {
currentApiConfigName: providerProfiles.currentApiConfigName,
})
// Default to the current config
// Default to the current config for the first selection
const defaultConfigName = providerProfiles.currentApiConfigName
setSelectedConfigName(defaultConfigName)
setConfigSelections([{ id: crypto.randomUUID(), configName: defaultConfigName, popoverOpen: false }])
// Apply the default config
const providerSettings = providerProfiles.apiConfigs[defaultConfigName] ?? {}
@ -373,22 +467,6 @@ export function NewRun() {
[clearErrors, setValue],
)
const onSelectConfig = useCallback(
(configName: string) => {
if (!importedSettings) {
return
}
setSelectedConfigName(configName)
setConfigPopoverOpen(false)
const providerSettings = importedSettings.apiConfigs[configName] ?? {}
setValue("model", getModelId(providerSettings) ?? "")
setValue("settings", { ...EVALS_SETTINGS, ...providerSettings, ...importedSettings.globalSettings })
},
[importedSettings, setValue],
)
return (
<>
<FormProvider {...form}>
@ -428,59 +506,91 @@ export function NewRun() {
onChange={onImportSettings}
/>
{importedSettings && Object.keys(importedSettings.apiConfigs).length > 1 && (
<div className="space-y-1">
<Label>API Config</Label>
<Popover open={configPopoverOpen} onOpenChange={setConfigPopoverOpen}>
<PopoverTrigger asChild>
<Button
variant="input"
role="combobox"
aria-expanded={configPopoverOpen}
className="flex items-center justify-between w-full">
<div>{selectedConfigName || "Select config"}</div>
<ChevronsUpDown className="opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-[var(--radix-popover-trigger-width)]">
<Command>
<CommandInput
placeholder="Search configs..."
className="h-9"
/>
<CommandList>
<CommandEmpty>No config found.</CommandEmpty>
<CommandGroup>
{Object.keys(importedSettings.apiConfigs).map(
(configName) => (
<CommandItem
key={configName}
value={configName}
onSelect={onSelectConfig}>
{configName}
{configName ===
importedSettings.currentApiConfigName && (
<span className="ml-2 text-xs text-muted-foreground">
(default)
</span>
)}
<Check
className={cn(
"ml-auto size-4",
configName ===
selectedConfigName
? "opacity-100"
: "opacity-0",
{importedSettings && Object.keys(importedSettings.apiConfigs).length > 0 && (
<div className="space-y-2">
<Label>API Configs</Label>
{configSelections.map((selection, index) => (
<div key={selection.id} className="flex items-center gap-2">
<Popover
open={selection.popoverOpen}
onOpenChange={(open) =>
toggleConfigPopover(selection.id, open)
}>
<PopoverTrigger asChild>
<Button
variant="input"
role="combobox"
aria-expanded={selection.popoverOpen}
className="flex items-center justify-between flex-1">
<div>{selection.configName || "Select config"}</div>
<ChevronsUpDown className="opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-[var(--radix-popover-trigger-width)]">
<Command>
<CommandInput
placeholder="Search configs..."
className="h-9"
/>
<CommandList>
<CommandEmpty>No config found.</CommandEmpty>
<CommandGroup>
{Object.keys(
importedSettings.apiConfigs,
).map((configName) => (
<CommandItem
key={configName}
value={configName}
onSelect={() =>
updateConfigSelection(
selection.id,
configName,
)
}>
{configName}
{configName ===
importedSettings.currentApiConfigName && (
<span className="ml-2 text-xs text-muted-foreground">
(default)
</span>
)}
/>
</CommandItem>
),
)}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Check
className={cn(
"ml-auto size-4",
configName ===
selection.configName
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{index === configSelections.length - 1 ? (
<Button
type="button"
variant="outline"
size="icon"
onClick={addConfigSelection}
className="shrink-0">
<Plus className="size-4" />
</Button>
) : (
<Button
type="button"
variant="outline"
size="icon"
onClick={() => removeConfigSelection(selection.id)}
className="shrink-0">
<Minus className="size-4" />
</Button>
)}
</div>
))}
</div>
)}
@ -501,18 +611,6 @@ export function NewRun() {
/>
<span className="text-sm">Use Native Tool Calls</span>
</label>
<label
htmlFor="multipleNativeToolCalls-other"
className="flex items-center gap-2 cursor-pointer">
<Checkbox
id="multipleNativeToolCalls-other"
checked={useMultipleNativeToolCalls}
onCheckedChange={(checked: boolean) =>
setUseMultipleNativeToolCalls(checked)
}
/>
<span className="text-sm">Use Multiple Native Tool Calls</span>
</label>
</div>
</div>
@ -522,110 +620,103 @@ export function NewRun() {
</div>
) : (
<>
<Popover open={modelPopoverOpen} onOpenChange={setModelPopoverOpen}>
<PopoverTrigger asChild>
<Button
variant="input"
role="combobox"
aria-expanded={modelPopoverOpen}
className="flex items-center justify-between">
<div>
{models?.find(({ id }) => id === model)?.name || `Select`}
</div>
<ChevronsUpDown className="opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-[var(--radix-popover-trigger-width)]">
<Command filter={onFilter}>
<CommandInput
placeholder="Search"
value={searchValue}
onValueChange={setSearchValue}
className="h-9"
<div className="space-y-2">
{modelSelections.map((selection, index) => (
<div key={selection.id} className="flex items-center gap-2">
<Popover
open={selection.popoverOpen}
onOpenChange={(open) => toggleModelPopover(selection.id, open)}>
<PopoverTrigger asChild>
<Button
variant="input"
role="combobox"
aria-expanded={selection.popoverOpen}
className="flex items-center justify-between flex-1">
<div>
{models?.find(({ id }) => id === selection.model)
?.name || `Select`}
</div>
<ChevronsUpDown className="opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-[var(--radix-popover-trigger-width)]">
<Command filter={onFilter}>
<CommandInput
placeholder="Search"
value={searchValue}
onValueChange={setSearchValue}
className="h-9"
/>
<CommandList>
<CommandEmpty>No model found.</CommandEmpty>
<CommandGroup>
{models?.map(({ id, name }) => (
<CommandItem
key={id}
value={id}
onSelect={() =>
updateModelSelection(
selection.id,
id,
)
}>
{name}
<Check
className={cn(
"ml-auto text-accent group-data-[selected=true]:text-accent-foreground size-4",
id === selection.model
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{index === modelSelections.length - 1 ? (
<Button
type="button"
variant="outline"
size="icon"
onClick={addModelSelection}
className="shrink-0">
<Plus className="size-4" />
</Button>
) : (
<Button
type="button"
variant="outline"
size="icon"
onClick={() => removeModelSelection(selection.id)}
className="shrink-0">
<Minus className="size-4" />
</Button>
)}
</div>
))}
</div>
<div className="mt-4 p-4 rounded-md bg-muted/30 border border-border space-y-3">
<Label className="text-sm font-medium text-muted-foreground">
Tool Protocol Options
</Label>
<div className="flex flex-col gap-2.5 pl-1">
<label
htmlFor="native"
className="flex items-center gap-2 cursor-pointer">
<Checkbox
id="native"
checked={useNativeToolProtocol}
onCheckedChange={(checked: boolean) =>
setUseNativeToolProtocol(checked)
}
/>
<CommandList>
<CommandEmpty>No model found.</CommandEmpty>
<CommandGroup>
{models?.map(({ id, name }) => (
<CommandItem
key={id}
value={id}
onSelect={onSelectModel}>
{name}
<Check
className={cn(
"ml-auto text-accent group-data-[selected=true]:text-accent-foreground size-4",
id === model ? "opacity-100" : "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<div className="mt-4 p-4 rounded-md bg-muted/30 border border-border space-y-4">
<div className="space-y-3">
<Label className="text-sm font-medium text-muted-foreground">
Tool Protocol Options
</Label>
<div className="flex flex-col gap-2.5 pl-1">
<label
htmlFor="native"
className="flex items-center gap-2 cursor-pointer">
<Checkbox
id="native"
checked={useNativeToolProtocol}
onCheckedChange={(checked: boolean) =>
setUseNativeToolProtocol(checked)
}
/>
<span className="text-sm">Use Native Tool Calls</span>
</label>
<label
htmlFor="multipleNativeToolCalls"
className="flex items-center gap-2 cursor-pointer">
<Checkbox
id="multipleNativeToolCalls"
checked={useMultipleNativeToolCalls}
onCheckedChange={(checked: boolean) =>
setUseMultipleNativeToolCalls(checked)
}
/>
<span className="text-sm">Use Multiple Native Tool Calls</span>
</label>
</div>
<span className="text-sm">Use Native Tool Calls</span>
</label>
</div>
{provider === "roo" && (
<div className="space-y-2 pt-2 border-t border-border">
<Label className="text-sm font-medium text-muted-foreground">
Reasoning Effort
</Label>
<Select
value={reasoningEffort || "none"}
onValueChange={(value) =>
setReasoningEffort(
value === "none" ? "" : (value as ReasoningEffort),
)
}>
<SelectTrigger className="w-full">
<SelectValue placeholder="None (default)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None (default)</SelectItem>
<SelectItem value="low">Low</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="high">High</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground pl-1">
When set, enableReasoningEffort will be automatically enabled
</p>
</div>
)}
</div>
</>
)}
@ -732,147 +823,153 @@ export function NewRun() {
)}
/>
<FormField
control={form.control}
name="concurrency"
render={({ field }) => (
<FormItem>
<FormLabel>Concurrency</FormLabel>
<FormControl>
<div className="flex flex-row items-center gap-2">
<Slider
value={[field.value]}
min={CONCURRENCY_MIN}
max={CONCURRENCY_MAX}
step={1}
onValueChange={(value) => {
field.onChange(value[0])
localStorage.setItem("evals-concurrency", String(value[0]))
}}
/>
<div>{field.value}</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Concurrency, Timeout, and Iterations in a 3-column row */}
<div className="grid grid-cols-3 gap-4 py-5">
<FormField
control={form.control}
name="concurrency"
render={({ field }) => (
<FormItem>
<FormLabel>Concurrency</FormLabel>
<FormControl>
<div className="flex flex-row items-center gap-2">
<Slider
value={[field.value]}
min={CONCURRENCY_MIN}
max={CONCURRENCY_MAX}
step={1}
onValueChange={(value) => {
field.onChange(value[0])
localStorage.setItem("evals-concurrency", String(value[0]))
}}
/>
<div className="w-6 text-right">{field.value}</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="timeout"
render={({ field }) => (
<FormItem>
<FormLabel>Timeout (Minutes)</FormLabel>
<FormControl>
<div className="flex flex-row items-center gap-2">
<Slider
value={[field.value]}
min={TIMEOUT_MIN}
max={TIMEOUT_MAX}
step={1}
onValueChange={(value) => {
field.onChange(value[0])
localStorage.setItem("evals-timeout", String(value[0]))
}}
/>
<div>{field.value}</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="timeout"
render={({ field }) => (
<FormItem>
<FormLabel>Timeout (Minutes)</FormLabel>
<FormControl>
<div className="flex flex-row items-center gap-2">
<Slider
value={[field.value]}
min={TIMEOUT_MIN}
max={TIMEOUT_MAX}
step={1}
onValueChange={(value) => {
field.onChange(value[0])
localStorage.setItem("evals-timeout", String(value[0]))
}}
/>
<div className="w-6 text-right">{field.value}</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="iterations"
render={({ field }) => (
<FormItem>
<FormLabel>Iterations per Exercise</FormLabel>
<FormControl>
<div className="flex flex-row items-center gap-2">
<Slider
value={[field.value]}
min={ITERATIONS_MIN}
max={ITERATIONS_MAX}
step={1}
onValueChange={(value) => {
field.onChange(value[0])
}}
/>
<div>{field.value}</div>
</div>
</FormControl>
<FormDescription>Run each exercise multiple times to compare results</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="iterations"
render={({ field }) => (
<FormItem>
<FormLabel>Iterations</FormLabel>
<FormControl>
<div className="flex flex-row items-center gap-2">
<Slider
value={[field.value]}
min={ITERATIONS_MIN}
max={ITERATIONS_MAX}
step={1}
onValueChange={(value) => {
field.onChange(value[0])
}}
/>
<div className="w-6 text-right">{field.value}</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormItem className="py-5">
<div className="flex items-center gap-1">
<Label>Terminal Command Timeout (Seconds)</Label>
<Tooltip>
<TooltipTrigger asChild>
<Info className="size-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p>
Maximum time in seconds to wait for terminal command execution to complete
before timing out. This applies to commands run via the execute_command tool.
</p>
</TooltipContent>
</Tooltip>
</div>
<div className="flex flex-row items-center gap-2">
<Slider
value={[commandExecutionTimeout]}
min={20}
max={60}
step={1}
onValueChange={([value]) => {
if (value !== undefined) {
setCommandExecutionTimeout(value)
localStorage.setItem("evals-command-execution-timeout", String(value))
}
}}
/>
<div className="w-8 text-right">{commandExecutionTimeout}</div>
</div>
</FormItem>
{/* Terminal timeouts in a 2-column row */}
<div className="grid grid-cols-2 gap-4 py-5">
<FormItem>
<div className="flex items-center gap-1">
<Label>Command Timeout (Seconds)</Label>
<Tooltip>
<TooltipTrigger asChild>
<Info className="size-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p>
Maximum time in seconds to wait for terminal command execution to complete
before timing out. This applies to commands run via the execute_command
tool.
</p>
</TooltipContent>
</Tooltip>
</div>
<div className="flex flex-row items-center gap-2">
<Slider
value={[commandExecutionTimeout]}
min={20}
max={60}
step={1}
onValueChange={([value]) => {
if (value !== undefined) {
setCommandExecutionTimeout(value)
localStorage.setItem("evals-command-execution-timeout", String(value))
}
}}
/>
<div className="w-8 text-right">{commandExecutionTimeout}</div>
</div>
</FormItem>
<FormItem className="py-5">
<div className="flex items-center gap-1">
<Label>Shell Integration Timeout (Seconds)</Label>
<Tooltip>
<TooltipTrigger asChild>
<Info className="size-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p>
Maximum time in seconds to wait for shell integration to initialize when opening
a new terminal.
</p>
</TooltipContent>
</Tooltip>
</div>
<div className="flex flex-row items-center gap-2">
<Slider
value={[terminalShellIntegrationTimeout]}
min={30}
max={60}
step={1}
onValueChange={([value]) => {
if (value !== undefined) {
setTerminalShellIntegrationTimeout(value)
localStorage.setItem("evals-shell-integration-timeout", String(value))
}
}}
/>
<div className="w-8 text-right">{terminalShellIntegrationTimeout}</div>
</div>
</FormItem>
<FormItem>
<div className="flex items-center gap-1">
<Label>Shell Integration Timeout (Seconds)</Label>
<Tooltip>
<TooltipTrigger asChild>
<Info className="size-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p>
Maximum time in seconds to wait for shell integration to initialize when
opening a new terminal.
</p>
</TooltipContent>
</Tooltip>
</div>
<div className="flex flex-row items-center gap-2">
<Slider
value={[terminalShellIntegrationTimeout]}
min={30}
max={60}
step={1}
onValueChange={([value]) => {
if (value !== undefined) {
setTerminalShellIntegrationTimeout(value)
localStorage.setItem("evals-shell-integration-timeout", String(value))
}
}}
/>
<div className="w-8 text-right">{terminalShellIntegrationTimeout}</div>
</div>
</FormItem>
</div>
<FormField
control={form.control}