FN-7768: inline thinking-level control in CustomModelDropdown and migrate task/agent surfaces
Adds an inline thinking-level selector to CustomModelDropdown and migrates existing model-selection surfaces (task form, quick entry, new agent dialog, model selection modal/tab) to use it instead of their own bespoke thinking-level controls. - CustomModelDropdown: add inline thinking-level control (CSS + component logic) - ModelSelectionModal / ModelSelectorTab: drop local thinking-level UI in favor of the dropdown's inline control - NewAgentDialog: migrate to the shared inline thinking-level control - QuickEntryBox: remove now-redundant thinking-level UI, delegate to CustomModelDropdown - TaskForm: migrate to the shared inline thinking-level control - Add/update tests for CustomModelDropdown, ModelSelectionModal, ModelSelectorTab, NewAgentDialog, QuickEntryBox, TaskForm - Add changeset for @runfusion/fusion (feature) - Update dashboard guide docs Files changed: .changeset/fn-7768-inline-thinking-dropdown.md | 7 ++ docs/dashboard-guide.md | 1 + .../app/components/CustomModelDropdown.css | 33 +++++++++ .../app/components/CustomModelDropdown.tsx | 69 ++++++++++++++++- .../app/components/ModelSelectionModal.tsx | 32 +------- .../dashboard/app/components/ModelSelectorTab.tsx | 33 +-------- .../dashboard/app/components/NewAgentDialog.tsx | 18 +---- .../dashboard/app/components/QuickEntryBox.tsx | 59 +-------------- packages/dashboard/app/components/TaskForm.tsx | 23 +----- .../__tests__/CustomModelDropdown.test.tsx | 72 ++++++++++++++++++ .../__tests__/ModelSelectionModal.test.tsx | 48 +++++++++--- .../components/__tests__/ModelSelectorTab.test.tsx | 3 +- .../components/__tests__/NewAgentDialog.test.tsx | 86 ++++++++++++++++++++++ .../components/__tests__/QuickEntryBox.test.tsx | 42 +++++++---- .../app/components/__tests__/TaskForm.test.tsx | 7 +- 15 files changed, 354 insertions(+), 179 deletions(-) Fusion-Task-Id: FN-7768 Fusion-Task-Lineage: 8f32731b-9865-4902-9420-62d3c0295805 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7768-inline-thinking-dropdown.md
Normal file
7
.changeset/fn-7768-inline-thinking-dropdown.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
summary: Add inline thinking-level selection to task and agent model dropdowns.
|
||||
category: feature
|
||||
dev: CustomModelDropdown now supports optional thinking-level props; migrated task and agent surfaces off standalone selects.
|
||||
@@ -480,6 +480,7 @@ Rules:
|
||||
- `auto-new` creates a branch after task creation using `fusion/{task-id}-{short-name}` (for example `fusion/fn-5671-branch-strategy-dropdown`).
|
||||
- `Merge target / base branch` stays optional for all modes and uses the same branch-dropdown + `Custom…` fallback behavior as Planning Mode.
|
||||
- In **More options → Model Configuration**, **Auto-merge** is a per-task override with three states: **Default** (follow project setting), **Enabled**, or **Disabled**.
|
||||
- In **More options → Model Configuration**, task and agent model pickers expose **Thinking Level** inside the same model dropdown panel instead of as a separate adjacent selector. Task pickers offer **Default (project setting)** plus **Off**, **Minimal**, **Low**, **Medium**, **High**, and **Very High**; agent creation is concrete-only and starts at **Off**.
|
||||
- In **More options → Model Configuration**, **Planner oversight** is a per-task override of the workflow-native `plannerOversightLevel` setting (FN-7508): **Inherit from workflow** (default) plus **Off**, **Observe**, **Steer**, and **Autonomous recovery**. This selector appears in both the New Task dialog and the Task Detail edit form (same shared control). Selecting **Inherit from workflow** clears the per-task override (sent as `null` on edit, omitted on create) so the task falls back to the effective `plannerOversightLevel` configured on its workflow — set project/global defaults for this in the **Workflow Editor → Values** tab, not in Project Settings; it is workflow-native, not a project setting.
|
||||
|
||||
The dialog also exposes AI handoffs that quick-add no longer shows: **Plan** opens Planning Mode with the current description, and **Subtask** opens Subtask Breakdown with the current description when **Settings → Experimental Features → Subtask Breakdown** is enabled. The Subtask handoff is hidden by default; visible handoff buttons remain disabled until the description has content, matching the quick-add row behavior for Subtask. **Execution mode** and optional workflow-step selection are available in the New Task dialog as well as quick entry, so users can choose Fast or standard execution and opt into workflow-specific creation-time steps before creating a task from either surface.
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
|
||||
.model-combobox-trigger-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -130,6 +131,38 @@
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.model-combobox-thinking {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
flex-shrink: 0;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.model-combobox-thinking-label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.model-combobox-thinking-select {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.model-combobox-thinking-badge {
|
||||
flex-shrink: 0;
|
||||
margin-left: var(--space-sm);
|
||||
max-width: 45%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:ModelDropdown 2026-07-09-00:00:
|
||||
FN-7760 requires the portaled model list to remain the touch-scroll owner on mobile. The global mobile lockdown keeps body/root overflow hidden and defaults touch gestures to vertical panning, so this fixed-position scroller needs its own iOS momentum scrolling, contained overscroll, and explicit vertical pan contract.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import "./CustomModelDropdown.css";
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import { useState, useEffect, useCallback, useMemo, useRef, useId } from "react";
|
||||
import { THINKING_LEVELS } from "@fusion/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createPortal } from "react-dom";
|
||||
import type { ModelInfo } from "../api";
|
||||
@@ -30,6 +31,14 @@ export interface CustomModelDropdownProps {
|
||||
onToggleModelFavorite?: (modelId: string) => void;
|
||||
/** Request a wider menu for dense settings surfaces while default callers keep trigger-width sizing. */
|
||||
menuWidth?: "trigger" | "readable";
|
||||
/** Optional thinking/reasoning effort value; empty string means inherit/default when defaultThinkingLevel is provided. */
|
||||
thinkingLevel?: string;
|
||||
/** Called when the optional inline thinking-level selector changes. */
|
||||
onThinkingLevelChange?: (level: string) => void;
|
||||
/** Effective default thinking level; when supplied, the selector includes an empty "Default (level)" option. */
|
||||
defaultThinkingLevel?: string;
|
||||
/** Explicitly render the optional inline thinking-level selector even without a change callback. */
|
||||
showThinkingLevel?: boolean;
|
||||
}
|
||||
|
||||
interface DropdownPosition {
|
||||
@@ -68,6 +77,10 @@ export function CustomModelDropdown({
|
||||
noChangeLabel: noChangeLabelProp,
|
||||
defaultOptionLabel: defaultOptionLabelProp,
|
||||
menuWidth = "trigger",
|
||||
thinkingLevel,
|
||||
onThinkingLevelChange,
|
||||
defaultThinkingLevel,
|
||||
showThinkingLevel,
|
||||
}: CustomModelDropdownProps) {
|
||||
const { t } = useTranslation("app");
|
||||
const placeholder = placeholderProp ?? t("model.selectPlaceholder", "Select a model…");
|
||||
@@ -78,6 +91,7 @@ export function CustomModelDropdown({
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
||||
const [dropdownPosition, setDropdownPosition] = useState<DropdownPosition | null>(null);
|
||||
const [portalRoot, setPortalRoot] = useState<HTMLElement | null>(null);
|
||||
const generatedThinkingId = useId();
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
@@ -137,6 +151,30 @@ export function CustomModelDropdown({
|
||||
}, [modelsByProvider, favoriteProviders]);
|
||||
|
||||
const hasNoChangeOption = typeof noChangeValue === "string" && noChangeValue.length > 0;
|
||||
const shouldShowThinking = showThinkingLevel ?? Boolean(onThinkingLevelChange);
|
||||
const normalizedThinkingLevel = thinkingLevel ?? "";
|
||||
const hasDefaultThinkingOption = typeof defaultThinkingLevel === "string";
|
||||
const thinkingSelectId = id ? `${id}-thinking-level` : `${generatedThinkingId}-thinking-level`;
|
||||
|
||||
/*
|
||||
FNXC:Settings-ThinkingLevel 2026-07-10-00:00:
|
||||
The shared model dropdown can optionally embed a thinking-level selector so task and agent model pickers expose one consistent reasoning-effort affordance, including `xhigh`. The selector stays inert unless a caller opts in with `showThinkingLevel` or `onThinkingLevelChange`, preserving every settings, insights, schedule, workflow, planning, onboarding, and bulk-edit surface that only needs model selection.
|
||||
*/
|
||||
const thinkingOptions = useMemo(() => THINKING_LEVELS.map((level) => ({
|
||||
value: level,
|
||||
label: t(`models.options.${level}`, level === "xhigh" ? "Very High" : level.charAt(0).toUpperCase() + level.slice(1)),
|
||||
})), [t]);
|
||||
|
||||
const thinkingBadgeLabel = useMemo(() => {
|
||||
if (!shouldShowThinking) return "";
|
||||
if (normalizedThinkingLevel) {
|
||||
return thinkingOptions.find((option) => option.value === normalizedThinkingLevel)?.label ?? normalizedThinkingLevel;
|
||||
}
|
||||
if (hasDefaultThinkingOption) {
|
||||
return t("modelSelection.thinkingDefault", "Default ({{level}})", { level: defaultThinkingLevel });
|
||||
}
|
||||
return thinkingOptions.find((option) => option.value === "off")?.label ?? "Off";
|
||||
}, [defaultThinkingLevel, hasDefaultThinkingOption, normalizedThinkingLevel, shouldShowThinking, t, thinkingOptions]);
|
||||
|
||||
// Get current provider from value
|
||||
const currentProvider = useMemo(() => {
|
||||
@@ -530,6 +568,30 @@ export function CustomModelDropdown({
|
||||
{t("models.count", { count: filteredModels.length, defaultValue_one: "{{count}} model", defaultValue_other: "{{count}} models" })}
|
||||
</div>
|
||||
|
||||
{shouldShowThinking && (
|
||||
<div className="model-combobox-thinking" onMouseDown={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}>
|
||||
<label className="model-combobox-thinking-label" htmlFor={thinkingSelectId}>
|
||||
{t("models.labels.thinkingLevel", "Thinking Level")}
|
||||
</label>
|
||||
<select
|
||||
id={thinkingSelectId}
|
||||
className="thinking-level-select model-combobox-thinking-select"
|
||||
data-testid="custom-model-dropdown-thinking"
|
||||
value={normalizedThinkingLevel}
|
||||
onChange={(e) => onThinkingLevelChange?.(e.target.value)}
|
||||
disabled={disabled || !onThinkingLevelChange}
|
||||
aria-label={t("models.labels.thinkingLevel", "Thinking Level")}
|
||||
>
|
||||
{hasDefaultThinkingOption && (
|
||||
<option value="">{t("modelSelection.thinkingDefault", "Default ({{level}})", { level: defaultThinkingLevel })}</option>
|
||||
)}
|
||||
{thinkingOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>{option.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={listRef} className="model-combobox-list">
|
||||
{specialOptions.map((option, index) => (
|
||||
<div
|
||||
@@ -691,6 +753,11 @@ export function CustomModelDropdown({
|
||||
</span>
|
||||
)}
|
||||
<span className="model-combobox-trigger-text">{selectedDisplayText || placeholder}</span>
|
||||
{shouldShowThinking && (
|
||||
<span className={`model-badge ${normalizedThinkingLevel ? "model-badge-custom" : "model-badge-default"} model-combobox-thinking-badge`} data-testid="custom-model-dropdown-thinking-badge">
|
||||
{thinkingBadgeLabel}
|
||||
</span>
|
||||
)}
|
||||
<span className="model-combobox-trigger-arrow">▼</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -296,6 +296,9 @@ export function ModelSelectionModal({
|
||||
onToggleFavorite={onToggleFavorite}
|
||||
favoriteModels={favoriteModels}
|
||||
onToggleModelFavorite={onToggleModelFavorite}
|
||||
thinkingLevel={thinkingLevel}
|
||||
onThinkingLevelChange={onThinkingLevelChange}
|
||||
defaultThinkingLevel={defaultThinkingLevel ?? "off"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -326,35 +329,6 @@ export function ModelSelectionModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{onThinkingLevelChange && (
|
||||
<div className="task-detail-section">
|
||||
<div className="inline-create-model-row">
|
||||
<label htmlFor="model-selection-thinking" className="inline-create-model-label">
|
||||
{t("modelSelection.thinkingModel", "Thinking")}
|
||||
</label>
|
||||
<span
|
||||
className={`model-badge ${thinkingLevel ? "model-badge-custom" : "model-badge-default"}`}
|
||||
data-testid="thinking-badge"
|
||||
>
|
||||
{thinkingLevel || t("modelSelection.usingDefault", "Using default")}
|
||||
</span>
|
||||
<select
|
||||
id="model-selection-thinking"
|
||||
data-testid="model-selection-thinking"
|
||||
value={thinkingLevel}
|
||||
onChange={(e) => onThinkingLevelChange(e.target.value)}
|
||||
>
|
||||
<option value="">{t("modelSelection.thinkingDefault", "Default ({{level}})", { level: defaultThinkingLevel ?? "off" })}</option>
|
||||
<option value="off">{t("models.options.off", "Off")}</option>
|
||||
<option value="minimal">{t("models.options.minimal", "Minimal")}</option>
|
||||
<option value="low">{t("models.options.low", "Low")}</option>
|
||||
<option value="medium">{t("models.options.medium", "Medium")}</option>
|
||||
<option value="high">{t("models.options.high", "High")}</option>
|
||||
<option value="xhigh">{t("models.options.xhigh", "Very High")}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -415,6 +415,9 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings, proj
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
favoriteModels={favoriteModels}
|
||||
onToggleModelFavorite={handleToggleModelFavorite}
|
||||
thinkingLevel={selectedThinking ?? ""}
|
||||
onThinkingLevelChange={handleThinkingChange}
|
||||
defaultThinkingLevel={settings?.defaultThinkingLevel ?? "off"}
|
||||
/>
|
||||
<small>{t("models.descriptions.executor", "The AI model used to implement this task.")}</small>
|
||||
</div>
|
||||
@@ -479,36 +482,6 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings, proj
|
||||
<small>{t("models.descriptions.planning", "The AI model used for task specification (triage phase).")}</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="thinkingLevel">{t("models.labels.thinkingLevel", "Thinking Level")}</label>
|
||||
<div className="model-selector-current">
|
||||
{savedThinking === null ? (
|
||||
<span className="model-badge model-badge-default">
|
||||
{t("models.states.usingDefault", "Using default")} ({settings?.defaultThinkingLevel ?? "off"})
|
||||
</span>
|
||||
) : (
|
||||
<span className="model-badge model-badge-custom">
|
||||
{savedThinking}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<select
|
||||
id="thinkingLevel"
|
||||
value={selectedThinking ?? ""}
|
||||
onChange={(e) => handleThinkingChange(e.target.value)}
|
||||
disabled={isSaving}
|
||||
className="thinking-level-select"
|
||||
>
|
||||
<option value="">{t("models.options.default", "Default")} ({settings?.defaultThinkingLevel ?? "off"})</option>
|
||||
<option value="off">{t("models.options.off", "Off")}</option>
|
||||
<option value="minimal">{t("models.options.minimal", "Minimal")}</option>
|
||||
<option value="low">{t("models.options.low", "Low")}</option>
|
||||
<option value="medium">{t("models.options.medium", "Medium")}</option>
|
||||
<option value="high">{t("models.options.high", "High")}</option>
|
||||
<option value="xhigh">{t("models.options.xhigh", "Very High")}</option>
|
||||
</select>
|
||||
<small>{t("models.descriptions.thinkingLevel", "Controls the reasoning effort for the AI agent. Higher levels use more tokens.")}</small>
|
||||
</div>
|
||||
|
||||
<div className="model-selector-status">
|
||||
{executorUsingDefault && validatorUsingDefault && planningUsingDefault && savedThinking === null
|
||||
|
||||
@@ -369,6 +369,8 @@ export function NewAgentDialog({
|
||||
onToggleFavorite={toggleFavoriteProvider}
|
||||
favoriteModels={favoriteModels}
|
||||
onToggleModelFavorite={toggleFavoriteModel}
|
||||
thinkingLevel={runtimeConfig.thinkingLevel}
|
||||
onThinkingLevelChange={(level) => setRuntimeConfig(c => ({ ...c, thinkingLevel: level as ThinkingLevel }))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -674,22 +676,6 @@ export function NewAgentDialog({
|
||||
{step === 1 && (
|
||||
<div>
|
||||
{renderRuntimeSourceSection("agent-runtime-source-step-1")}
|
||||
<div className="agent-dialog-field">
|
||||
<label htmlFor="agent-thinking">{t("agents.fieldThinkingLevel", "Thinking Level")}</label>
|
||||
<select
|
||||
id="agent-thinking"
|
||||
className="select"
|
||||
value={runtimeConfig.thinkingLevel}
|
||||
onChange={e => setRuntimeConfig(c => ({ ...c, thinkingLevel: e.target.value as ThinkingLevel }))}
|
||||
>
|
||||
<option value="off">{t("agents.thinkingOff", "Off")}</option>
|
||||
<option value="minimal">{t("agents.thinkingMinimal", "Minimal")}</option>
|
||||
<option value="low">{t("agents.thinkingLow", "Low")}</option>
|
||||
<option value="medium">{t("agents.thinkingMedium", "Medium")}</option>
|
||||
<option value="high">{t("agents.thinkingHigh", "High")}</option>
|
||||
<option value="xhigh">{t("agents.thinkingXhigh", "Very High")}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="agent-dialog-field">
|
||||
<label htmlFor="agent-max-turns">{t("agents.fieldMaxTurns", "Max Turns")}</label>
|
||||
<input
|
||||
|
||||
@@ -170,7 +170,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
const [showPriorityPicker, setShowPriorityPicker] = useState(false);
|
||||
const [agentsLoading, setAgentsLoading] = useState(false);
|
||||
const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);
|
||||
const [activeModelSubmenu, setActiveModelSubmenu] = useState<"plan" | "executor" | "validator" | "thinking" | null>(null);
|
||||
const [activeModelSubmenu, setActiveModelSubmenu] = useState<"plan" | "executor" | "validator" | null>(null);
|
||||
const [executorProvider, setExecutorProvider] = useState<string | undefined>(undefined);
|
||||
const [executorModelId, setExecutorModelId] = useState<string | undefined>(undefined);
|
||||
const [validatorProvider, setValidatorProvider] = useState<string | undefined>(undefined);
|
||||
@@ -415,7 +415,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
const hasExecutorOverride = Boolean(executorProvider && executorModelId);
|
||||
const hasValidatorOverride = Boolean(validatorProvider && validatorModelId);
|
||||
const hasPlanningOverride = Boolean(planningProvider && planningModelId);
|
||||
const hasThinkingOverride = Boolean(thinkingLevel);
|
||||
const selectedModelCount = Number(hasExecutorOverride) + Number(hasValidatorOverride) + Number(hasPlanningOverride);
|
||||
const modelMenuLabel = selectedPresetId
|
||||
? settings?.modelPresets?.find((p) => p.id === selectedPresetId)?.name ?? t("tasks.models", "Models")
|
||||
@@ -2351,59 +2350,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
</span>
|
||||
<ChevronRight size={12} style={{ marginLeft: "auto", color: "var(--text-dim)" }} />
|
||||
</button>
|
||||
{/*
|
||||
FNXC:Settings-ThinkingLevel 2026-07-09-00:00:
|
||||
Quick-entry inline model menu must expose the same thinking (reasoning-effort) levels as the full task
|
||||
pickers (TaskForm, ModelSelectorTab, ModelSelectionModal) so a task created from this bar can carry a
|
||||
per-task thinking-level override just like one created from the New Task modal.
|
||||
*/}
|
||||
<button
|
||||
type="button"
|
||||
className={`model-menu-item ${hasThinkingOverride ? "model-menu-item--active" : ""}`}
|
||||
onClick={() => setActiveModelSubmenu("thinking")}
|
||||
data-testid="model-menu-thinking"
|
||||
>
|
||||
<span className="model-menu-item-label">
|
||||
<Brain size={12} style={{ verticalAlign: "middle", marginRight: 6 }} />
|
||||
{t("tasks.modelThinking", "Thinking")}
|
||||
</span>
|
||||
<span className="model-menu-item-value">
|
||||
{hasThinkingOverride
|
||||
? thinkingLevel
|
||||
: t("tasks.usingDefault", "Using default")}
|
||||
</span>
|
||||
<ChevronRight size={12} style={{ marginLeft: "auto", color: "var(--text-dim)" }} />
|
||||
</button>
|
||||
</div>
|
||||
) : activeModelSubmenu === "thinking" ? (
|
||||
// Submenu with a plain <select> for the thinking-level override (not a model list)
|
||||
<div className="model-submenu">
|
||||
<button
|
||||
type="button"
|
||||
className="model-submenu-back"
|
||||
onClick={() => setActiveModelSubmenu(null)}
|
||||
data-testid="model-submenu-back"
|
||||
>
|
||||
<ChevronDown size={12} style={{ transform: "rotate(90deg)", marginRight: 4 }} />
|
||||
{t("common.back", "Back")}
|
||||
</button>
|
||||
<div className="model-submenu-header">
|
||||
{t("tasks.thinkingModel", "Thinking Level")}
|
||||
</div>
|
||||
<select
|
||||
id="model-thinking-select"
|
||||
data-testid="model-thinking-select"
|
||||
value={thinkingLevel}
|
||||
onChange={(e) => handleThinkingLevelChange(e.target.value)}
|
||||
>
|
||||
<option value="">{t("modelSelection.thinkingDefault", "Default ({{level}})", { level: settings?.defaultThinkingLevel ?? "off" })}</option>
|
||||
<option value="off">{t("models.options.off", "Off")}</option>
|
||||
<option value="minimal">{t("models.options.minimal", "Minimal")}</option>
|
||||
<option value="low">{t("models.options.low", "Low")}</option>
|
||||
<option value="medium">{t("models.options.medium", "Medium")}</option>
|
||||
<option value="high">{t("models.options.high", "High")}</option>
|
||||
<option value="xhigh">{t("models.options.xhigh", "Very High")}</option>
|
||||
</select>
|
||||
</div>
|
||||
) : (
|
||||
// Submenu with CustomModelDropdown for the selected target
|
||||
@@ -2446,6 +2392,9 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
favoriteModels={effectiveFavoriteModels}
|
||||
onToggleModelFavorite={handleToggleModelFavorite}
|
||||
thinkingLevel={activeModelSubmenu === "executor" ? thinkingLevel : undefined}
|
||||
onThinkingLevelChange={activeModelSubmenu === "executor" ? handleThinkingLevelChange : undefined}
|
||||
defaultThinkingLevel={activeModelSubmenu === "executor" ? settings?.defaultThinkingLevel ?? "off" : undefined}
|
||||
/>
|
||||
{modelsError && (
|
||||
<div className="model-submenu-error">
|
||||
|
||||
@@ -1527,6 +1527,9 @@ export function TaskForm({
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
favoriteModels={favoriteModels}
|
||||
onToggleModelFavorite={handleToggleModelFavorite}
|
||||
thinkingLevel={thinkingLevel || ""}
|
||||
onThinkingLevelChange={onThinkingLevelChange ? (value) => onThinkingLevelChange(value) : undefined}
|
||||
defaultThinkingLevel={settings?.defaultThinkingLevel ?? "off"}
|
||||
/>
|
||||
</div>
|
||||
<div className="model-select-row">
|
||||
@@ -1569,26 +1572,6 @@ export function TaskForm({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{onThinkingLevelChange && (
|
||||
<div className="model-select-row">
|
||||
{/* FNXC:Settings-ThinkingLevel 2026-06-19-14:55: The shared task thinking selector must expose `xhigh` so new-task and task-detail edits can request maximum reasoning effort instead of being capped at `high`. */}
|
||||
<label htmlFor="thinking-level" className="model-select-label">{t("taskForm.thinkingLabel", "Thinking")}</label>
|
||||
<select
|
||||
id="thinking-level"
|
||||
value={thinkingLevel || ""}
|
||||
onChange={(e) => onThinkingLevelChange(e.target.value)}
|
||||
disabled={disabled || presetMode === "preset"}
|
||||
>
|
||||
<option value="">{t("taskForm.thinkingDefault", "Default ({{level}})", { level: settings?.defaultThinkingLevel ?? "off" })}</option>
|
||||
<option value="off">{t("taskForm.thinkingOff", "Off")}</option>
|
||||
<option value="minimal">{t("taskForm.thinkingMinimal", "Minimal")}</option>
|
||||
<option value="low">{t("taskForm.thinkingLow", "Low")}</option>
|
||||
<option value="medium">{t("taskForm.thinkingMedium", "Medium")}</option>
|
||||
<option value="high">{t("taskForm.thinkingHigh", "High")}</option>
|
||||
<option value="xhigh">{t("taskForm.thinkingXhigh", "Very High")}</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{onPlannerOversightLevelChange && (
|
||||
<div className="model-select-row">
|
||||
{/* FNXC:PlannerOversight 2026-07-04-00:00: Per-task override for the workflow-native plannerOversightLevel setting (FN-7508). Empty value inherits the workflow's effective value; the four levels mirror BUILTIN_OVERSIGHT_SETTINGS verbatim. Configuration only — runtime controls are FN-7517. */}
|
||||
|
||||
@@ -50,6 +50,78 @@ describe("CustomModelDropdown", () => {
|
||||
expect(css).not.toMatch(/(^|\n)\s*html\s*\*/);
|
||||
});
|
||||
|
||||
it("renders opt-in thinking control with default option and calls back for concrete and inherited values", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
const onThinkingLevelChange = vi.fn();
|
||||
|
||||
render(
|
||||
<CustomModelDropdown
|
||||
label="Executor Model"
|
||||
value="openai/gpt-4o"
|
||||
onChange={onChange}
|
||||
models={MOCK_MODELS}
|
||||
thinkingLevel="high"
|
||||
onThinkingLevelChange={onThinkingLevelChange}
|
||||
defaultThinkingLevel="off"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("custom-model-dropdown-thinking-badge")).toHaveTextContent("High");
|
||||
await user.click(screen.getByRole("button", { name: "Executor Model" }));
|
||||
|
||||
const thinkingSelect = await screen.findByTestId("custom-model-dropdown-thinking");
|
||||
expect(thinkingSelect).toHaveAccessibleName("Thinking Level");
|
||||
expect(within(thinkingSelect).getByRole("option", { name: "Default (off)" })).toBeTruthy();
|
||||
for (const optionName of ["Off", "Minimal", "Low", "Medium", "High", "Very High"]) {
|
||||
expect(within(thinkingSelect).getByRole("option", { name: optionName })).toBeTruthy();
|
||||
}
|
||||
|
||||
await user.selectOptions(thinkingSelect, "xhigh");
|
||||
expect(onThinkingLevelChange).toHaveBeenLastCalledWith("xhigh");
|
||||
await user.selectOptions(thinkingSelect, "");
|
||||
expect(onThinkingLevelChange).toHaveBeenLastCalledWith("");
|
||||
});
|
||||
|
||||
it("renders concrete-only thinking control without Default when no defaultThinkingLevel is supplied", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<CustomModelDropdown
|
||||
label="Agent Model"
|
||||
value=""
|
||||
onChange={vi.fn()}
|
||||
models={MOCK_MODELS}
|
||||
thinkingLevel="off"
|
||||
onThinkingLevelChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Agent Model" }));
|
||||
const thinkingSelect = await screen.findByTestId("custom-model-dropdown-thinking");
|
||||
|
||||
expect(within(thinkingSelect).queryByRole("option", { name: /Default/ })).toBeNull();
|
||||
expect(within(thinkingSelect).getAllByRole("option")).toHaveLength(6);
|
||||
});
|
||||
|
||||
it("keeps thinking control inert when callers do not opt in", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<CustomModelDropdown
|
||||
label="Settings Model"
|
||||
value=""
|
||||
onChange={vi.fn()}
|
||||
models={MOCK_MODELS}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("custom-model-dropdown-thinking-badge")).toBeNull();
|
||||
await user.click(screen.getByRole("button", { name: "Settings Model" }));
|
||||
|
||||
expect(screen.queryByTestId("custom-model-dropdown-thinking")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders the open dropdown in a portal attached to document.body", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
|
||||
@@ -68,6 +68,9 @@ vi.mock("../CustomModelDropdown", () => ({
|
||||
onChange,
|
||||
models,
|
||||
placeholder,
|
||||
thinkingLevel,
|
||||
onThinkingLevelChange,
|
||||
defaultThinkingLevel,
|
||||
}: {
|
||||
id: string;
|
||||
label: string;
|
||||
@@ -75,10 +78,33 @@ vi.mock("../CustomModelDropdown", () => ({
|
||||
onChange: (value: string) => void;
|
||||
models: ModelInfo[];
|
||||
placeholder: string;
|
||||
thinkingLevel?: string;
|
||||
onThinkingLevelChange?: (value: string) => void;
|
||||
defaultThinkingLevel?: string;
|
||||
}) => (
|
||||
<div data-testid={`mock-dropdown-${id}`}>
|
||||
<span data-testid={`dropdown-label-${id}`}>{label}</span>
|
||||
<span data-testid={`dropdown-value-${id}`}>{value || "empty"}</span>
|
||||
{onThinkingLevelChange ? (
|
||||
<>
|
||||
<span data-testid="custom-model-dropdown-thinking-badge" className={`model-badge ${thinkingLevel ? "model-badge-custom" : "model-badge-default"}`}>
|
||||
{thinkingLevel || "Default"}
|
||||
</span>
|
||||
<select
|
||||
data-testid="custom-model-dropdown-thinking"
|
||||
value={thinkingLevel || ""}
|
||||
onChange={(e) => onThinkingLevelChange(e.target.value)}
|
||||
>
|
||||
<option value="">Default ({defaultThinkingLevel ?? "off"})</option>
|
||||
<option value="off">Off</option>
|
||||
<option value="minimal">Minimal</option>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
<option value="xhigh">Very High</option>
|
||||
</select>
|
||||
</>
|
||||
) : null}
|
||||
<select
|
||||
data-testid={`dropdown-select-${id}`}
|
||||
value={value}
|
||||
@@ -280,14 +306,14 @@ describe("ModelSelectionModal", () => {
|
||||
describe("Thinking level selection", () => {
|
||||
it("does not render the thinking-level selector when onThinkingLevelChange is omitted", () => {
|
||||
renderModelSelectionModal();
|
||||
expect(screen.queryByTestId("model-selection-thinking")).toBeNull();
|
||||
expect(screen.queryByTestId("thinking-badge")).toBeNull();
|
||||
expect(screen.queryByTestId("custom-model-dropdown-thinking")).toBeNull();
|
||||
expect(screen.queryByTestId("custom-model-dropdown-thinking-badge")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders the thinking-level selector with all six levels plus Default when onThinkingLevelChange is provided", () => {
|
||||
renderModelSelectionModal({ onThinkingLevelChange: vi.fn() });
|
||||
|
||||
const select = screen.getByTestId("model-selection-thinking") as HTMLSelectElement;
|
||||
const select = screen.getByTestId("custom-model-dropdown-thinking") as HTMLSelectElement;
|
||||
expect(select).toBeTruthy();
|
||||
|
||||
const options = Array.from(select.options).map((o) => o.value);
|
||||
@@ -296,24 +322,24 @@ describe("ModelSelectionModal", () => {
|
||||
|
||||
it("still omits the thinking-level selector while models are empty, even when onThinkingLevelChange is provided", () => {
|
||||
renderModelSelectionModal({ models: [], onThinkingLevelChange: vi.fn() });
|
||||
expect(screen.queryByTestId("model-selection-thinking")).toBeNull();
|
||||
expect(screen.queryByTestId("custom-model-dropdown-thinking")).toBeNull();
|
||||
});
|
||||
|
||||
it("still omits the thinking-level selector while models are loading", () => {
|
||||
renderModelSelectionModal({ modelsLoading: true, onThinkingLevelChange: vi.fn() });
|
||||
expect(screen.queryByTestId("model-selection-thinking")).toBeNull();
|
||||
expect(screen.queryByTestId("custom-model-dropdown-thinking")).toBeNull();
|
||||
});
|
||||
|
||||
it("still omits the thinking-level selector when models fail to load", () => {
|
||||
renderModelSelectionModal({ modelsError: "Failed to fetch", onThinkingLevelChange: vi.fn() });
|
||||
expect(screen.queryByTestId("model-selection-thinking")).toBeNull();
|
||||
expect(screen.queryByTestId("custom-model-dropdown-thinking")).toBeNull();
|
||||
});
|
||||
|
||||
it("calls onThinkingLevelChange with the selected level", () => {
|
||||
const onThinkingLevelChange = vi.fn();
|
||||
renderModelSelectionModal({ onThinkingLevelChange });
|
||||
|
||||
const select = screen.getByTestId("model-selection-thinking");
|
||||
const select = screen.getByTestId("custom-model-dropdown-thinking");
|
||||
fireEvent.change(select, { target: { value: "high" } });
|
||||
|
||||
expect(onThinkingLevelChange).toHaveBeenCalledWith("high");
|
||||
@@ -323,7 +349,7 @@ describe("ModelSelectionModal", () => {
|
||||
const onThinkingLevelChange = vi.fn();
|
||||
renderModelSelectionModal({ onThinkingLevelChange, thinkingLevel: "high" });
|
||||
|
||||
const select = screen.getByTestId("model-selection-thinking");
|
||||
const select = screen.getByTestId("custom-model-dropdown-thinking");
|
||||
fireEvent.change(select, { target: { value: "" } });
|
||||
|
||||
expect(onThinkingLevelChange).toHaveBeenCalledWith("");
|
||||
@@ -332,15 +358,15 @@ describe("ModelSelectionModal", () => {
|
||||
it("displays the thinking badge as 'Using default' when thinkingLevel is empty", () => {
|
||||
renderModelSelectionModal({ onThinkingLevelChange: vi.fn(), thinkingLevel: "" });
|
||||
|
||||
const badge = screen.getByTestId("thinking-badge");
|
||||
expect(badge.textContent).toBe("Using default");
|
||||
const badge = screen.getByTestId("custom-model-dropdown-thinking-badge");
|
||||
expect(badge.textContent).toBe("Default");
|
||||
expect(badge.classList.contains("model-badge-default")).toBe(true);
|
||||
});
|
||||
|
||||
it("displays the thinking badge with the selected level when overridden", () => {
|
||||
renderModelSelectionModal({ onThinkingLevelChange: vi.fn(), thinkingLevel: "xhigh" });
|
||||
|
||||
const badge = screen.getByTestId("thinking-badge");
|
||||
const badge = screen.getByTestId("custom-model-dropdown-thinking-badge");
|
||||
expect(badge.textContent).toBe("xhigh");
|
||||
expect(badge.classList.contains("model-badge-custom")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -168,7 +168,8 @@ describe("ModelSelectorTab", () => {
|
||||
}, "project-alpha");
|
||||
});
|
||||
|
||||
await user.selectOptions(screen.getByLabelText("Thinking Level"), "high");
|
||||
await user.click(screen.getByRole("button", { name: "Executor Model" }));
|
||||
await user.selectOptions(await screen.findByTestId("custom-model-dropdown-thinking"), "high");
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateTask).toHaveBeenNthCalledWith(4, "FN-7398", {
|
||||
thinkingLevel: "high",
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
|
||||
import { NewAgentDialog } from "../NewAgentDialog";
|
||||
|
||||
vi.mock("../api", async () => {
|
||||
const actual = await vi.importActual<typeof import("../api")>("../api");
|
||||
return {
|
||||
...actual,
|
||||
fetchModels: vi.fn().mockResolvedValue({
|
||||
models: [{ provider: "openai", id: "gpt-4o", name: "GPT-4o" }],
|
||||
}),
|
||||
fetchAgents: vi.fn().mockResolvedValue([]),
|
||||
fetchPluginRuntimes: vi.fn().mockResolvedValue({ runtimes: [] }),
|
||||
createAgent: vi.fn().mockResolvedValue({ id: "agent-1" }),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../CustomModelDropdown", () => ({
|
||||
CustomModelDropdown: ({
|
||||
thinkingLevel,
|
||||
onThinkingLevelChange,
|
||||
defaultThinkingLevel,
|
||||
}: {
|
||||
thinkingLevel?: string;
|
||||
onThinkingLevelChange?: (value: string) => void;
|
||||
defaultThinkingLevel?: string;
|
||||
}) => (
|
||||
<div data-testid="agent-model-dropdown">
|
||||
<select
|
||||
data-testid="custom-model-dropdown-thinking"
|
||||
value={thinkingLevel || ""}
|
||||
onChange={(e) => onThinkingLevelChange?.(e.target.value)}
|
||||
>
|
||||
{defaultThinkingLevel ? <option value="">Default ({defaultThinkingLevel})</option> : null}
|
||||
<option value="off">Off</option>
|
||||
<option value="minimal">Minimal</option>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
<option value="xhigh">Very High</option>
|
||||
</select>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("../SkillMultiselect", () => ({
|
||||
SkillMultiselect: () => <div data-testid="skill-multiselect" />,
|
||||
}));
|
||||
|
||||
vi.mock("../AgentGenerationModal", () => ({
|
||||
AgentGenerationModal: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("../ExperimentalAgentOnboardingModal", () => ({
|
||||
ExperimentalAgentOnboardingModal: () => null,
|
||||
}));
|
||||
|
||||
describe("NewAgentDialog thinking level", () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("uses CustomModelDropdown thinking control with concrete-only agent semantics", async () => {
|
||||
render(<NewAgentDialog isOpen onClose={vi.fn()} onCreated={vi.fn()} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId("agent-dialog-tab-custom"));
|
||||
const thinkingSelect = await screen.findByTestId("custom-model-dropdown-thinking") as HTMLSelectElement;
|
||||
|
||||
expect(Array.from(thinkingSelect.options).map((option) => option.value)).toEqual([
|
||||
"off",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
]);
|
||||
expect(thinkingSelect.value).toBe("off");
|
||||
expect(screen.queryByText(/Default/)).toBeNull();
|
||||
expect(screen.queryByLabelText("Thinking Level")).toBeNull();
|
||||
|
||||
fireEvent.change(thinkingSelect, { target: { value: "xhigh" } });
|
||||
|
||||
await waitFor(() => expect((screen.getByTestId("custom-model-dropdown-thinking") as HTMLSelectElement).value).toBe("xhigh"));
|
||||
});
|
||||
});
|
||||
@@ -245,6 +245,9 @@ vi.mock("../CustomModelDropdown", () => ({
|
||||
onChange,
|
||||
label,
|
||||
disabled,
|
||||
thinkingLevel,
|
||||
onThinkingLevelChange,
|
||||
defaultThinkingLevel,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
@@ -257,9 +260,28 @@ vi.mock("../CustomModelDropdown", () => ({
|
||||
onToggleFavorite?: (provider: string) => void;
|
||||
favoriteModels?: string[];
|
||||
onToggleModelFavorite?: (modelId: string) => void;
|
||||
thinkingLevel?: string;
|
||||
onThinkingLevelChange?: (value: string) => void;
|
||||
defaultThinkingLevel?: string;
|
||||
}) => (
|
||||
<div data-testid={`custom-model-dropdown-${label}`}>
|
||||
<span data-testid={`dropdown-value-${label}`}>{value || "none"}</span>
|
||||
{onThinkingLevelChange ? (
|
||||
<select
|
||||
data-testid="custom-model-dropdown-thinking"
|
||||
value={thinkingLevel || ""}
|
||||
onChange={(e) => onThinkingLevelChange(e.target.value)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<option value="">Default ({defaultThinkingLevel ?? "off"})</option>
|
||||
<option value="off">Off</option>
|
||||
<option value="minimal">Minimal</option>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
<option value="xhigh">Very High</option>
|
||||
</select>
|
||||
) : null}
|
||||
<button
|
||||
data-testid={`dropdown-select-${label}`}
|
||||
onClick={() => onChange("anthropic/claude-sonnet-4-5")}
|
||||
@@ -2587,13 +2609,7 @@ describe("QuickEntryBox", () => {
|
||||
expect(screen.getByTestId("model-menu-validator")).toBeTruthy();
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:Settings-ThinkingLevel 2026-07-09-00:00:
|
||||
The inline quick-entry model menu must expose a Thinking entry with parity to the full task pickers
|
||||
(TaskForm, ModelSelectorTab, ModelSelectionModal), including a plain <select> submenu with all six
|
||||
levels plus a named Default option.
|
||||
*/
|
||||
it("shows a Thinking option in the model menu", () => {
|
||||
it("does not render a separate Thinking option in the model menu", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
@@ -2601,19 +2617,19 @@ describe("QuickEntryBox", () => {
|
||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||
openModelMenu();
|
||||
|
||||
expect(screen.getByTestId("model-menu-thinking")).toBeTruthy();
|
||||
expect(screen.queryByTestId("model-menu-thinking")).toBeNull();
|
||||
});
|
||||
|
||||
it("clicking Thinking opens a submenu with a level <select>", () => {
|
||||
it("clicking Executor opens a submenu with the inline thinking-level selector", () => {
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task with models" } });
|
||||
openModelMenu();
|
||||
fireEvent.click(screen.getByTestId("model-menu-thinking"));
|
||||
fireEvent.click(screen.getByTestId("model-menu-executor"));
|
||||
|
||||
const select = screen.getByTestId("model-thinking-select") as HTMLSelectElement;
|
||||
const select = screen.getByTestId("custom-model-dropdown-thinking") as HTMLSelectElement;
|
||||
expect(select).toBeTruthy();
|
||||
const options = Array.from(select.options).map((o) => o.value);
|
||||
expect(options).toEqual(["", "off", "minimal", "low", "medium", "high", "xhigh"]);
|
||||
@@ -3009,8 +3025,8 @@ describe("QuickEntryBox", () => {
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task with thinking level override" } });
|
||||
openModelMenu();
|
||||
fireEvent.click(screen.getByTestId("model-menu-thinking"));
|
||||
fireEvent.change(screen.getByTestId("model-thinking-select"), { target: { value: "xhigh" } });
|
||||
fireEvent.click(screen.getByTestId("model-menu-executor"));
|
||||
fireEvent.change(screen.getByTestId("custom-model-dropdown-thinking"), { target: { value: "xhigh" } });
|
||||
fireEvent.click(screen.getByTestId("model-submenu-back"));
|
||||
|
||||
fireEvent.keyDown(textarea, { key: "Escape" });
|
||||
|
||||
@@ -154,9 +154,10 @@ describe("TaskForm", () => {
|
||||
|
||||
fireEvent.click(screen.getByTestId("task-form-more-options-toggle"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("combobox", { name: /Thinking/i })).toBeTruthy();
|
||||
});
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Executor Model" }));
|
||||
|
||||
const thinkingSelect = await screen.findByTestId("custom-model-dropdown-thinking");
|
||||
expect(thinkingSelect).toBeTruthy();
|
||||
expect(screen.getByRole("option", { name: /Very High/i })).toHaveAttribute("value", "xhigh");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user