FN-7746: Add thinking level selector to model pickers

Adds a thinking-level (reasoning-effort) selector to the quick-create model pickers, matching the levels already available in the full task pickers.

- ModelSelectionModal: adds optional thinking-level <select> (off/minimal/low/medium/high/xhigh) with a status badge, rendered only when onThinkingLevelChange is supplied
- QuickEntryBox: wires thinking-level state through to ModelSelectionModal and persists the override with quick-created tasks
- InlineCreateCard: same thinking-level wiring for the inline task creation card
- Adds i18n strings for the new thinking-level labels across en/es/fr/ko/zh-CN/zh-TW locale files
- Adds test coverage in InlineCreateCard.test.tsx, ModelSelectionModal.test.tsx, and QuickEntryBox.test.tsx for the new selector

Files changed:
 .../dashboard/app/components/InlineCreateCard.tsx  | 16 ++++-
 .../app/components/ModelSelectionModal.tsx         | 47 +++++++++++++-
 .../dashboard/app/components/QuickEntryBox.tsx     | 67 ++++++++++++++++++-
 .../components/__tests__/InlineCreateCard.test.tsx | 70 ++++++++++++++++++++
 .../__tests__/ModelSelectionModal.test.tsx         | 69 ++++++++++++++++++++
 .../components/__tests__/QuickEntryBox.test.tsx    | 75 ++++++++++++++++++++++
 packages/i18n/locales/en/app.json                  |  2 +
 packages/i18n/locales/es/app.json                  |  2 +
 packages/i18n/locales/fr/app.json                  |  2 +
 packages/i18n/locales/ko/app.json                  |  2 +
 packages/i18n/locales/zh-CN/app.json               |  2 +
 packages/i18n/locales/zh-TW/app.json               |  2 +
 12 files changed, 351 insertions(+), 5 deletions(-)

Fusion-Task-Id: FN-7746

Fusion-Task-Lineage: 4a221203-0222-47d6-93f8-632638bcfbfe

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-09 13:51:45 -07:00
parent f9307904c2
commit 2c4052c76b
12 changed files with 351 additions and 5 deletions

View File

@@ -3,7 +3,7 @@ import { useState, useCallback, useEffect, useMemo, useRef } from "react";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
import { Brain, Link, ListTree, Zap, ChevronDown, ChevronUp, Bot, Maximize2, Minimize2, Server } from "lucide-react";
import { DEFAULT_TASK_PRIORITY, TASK_PRIORITIES, type Task, type TaskPriority, type Settings, type ResolvedWorkflowOptionalStep } from "@fusion/core";
import { DEFAULT_TASK_PRIORITY, TASK_PRIORITIES, type Task, type TaskPriority, type Settings, type ResolvedWorkflowOptionalStep, type ThinkingLevel } from "@fusion/core";
import { getErrorMessage } from "@fusion/core";
import type { ToastType } from "../hooks/useToast";
import { checkDuplicateTasks, fetchModels, uploadAttachment, fetchSettings, updateGlobalSettings, fetchAgents, fetchWorkflowOptionalSteps, DuplicateCandidatesError } from "../api";
@@ -122,6 +122,8 @@ export function InlineCreateCard({
const [validatorModelId, setValidatorModelId] = useState<string | undefined>(undefined);
const [planningProvider, setPlanningProvider] = useState<string | undefined>(undefined);
const [planningModelId, setPlanningModelId] = useState<string | undefined>(undefined);
/* FNXC:Settings-ThinkingLevel 2026-07-09-00:00: quick-create board card carries the same per-task thinking-level override as the full New Task modal; "" means "use default". */
const [thinkingLevel, setThinkingLevel] = useState<string>("");
const [optionalSteps, setOptionalSteps] = useState<ResolvedWorkflowOptionalStep[]>([]);
const [enabledOptionalStepIds, setEnabledOptionalStepIds] = useState<string[]>([]);
const [priority, setPriority] = useState<TaskPriority>(DEFAULT_TASK_PRIORITY);
@@ -433,6 +435,7 @@ export function InlineCreateCard({
setValidatorModelId(undefined);
setPlanningProvider(undefined);
setPlanningModelId(undefined);
setThinkingLevel("");
setEnabledOptionalStepIds([]);
setPriority(DEFAULT_TASK_PRIORITY);
setDependencies([]);
@@ -490,6 +493,7 @@ export function InlineCreateCard({
validatorModelId: hasValidatorOverride ? validatorModelId : undefined,
planningModelProvider: hasPlanningOverride ? planningProvider : undefined,
planningModelId: hasPlanningOverride ? planningModelId : undefined,
thinkingLevel: thinkingLevel !== "" ? (thinkingLevel as ThinkingLevel) : undefined,
/*
FNXC:InlineCreateWorkflowSteps 2026-06-29-02:45:
Inline create optional-step toggles are explicit task intent. When a workflow exposes optional steps and the operator unchecks all of them, submit `[]` so default-on Plan Review / Code Review stay disabled on the created task instead of reappearing from workflow defaults.
@@ -511,7 +515,7 @@ export function InlineCreateCard({
}
await submitTask(input);
}, [description, submitting, selectedWorkflowId, dependencies, selectedAgentId, selectedPresetId, hasExecutorOverride, executorProvider, executorModelId, hasValidatorOverride, validatorProvider, validatorModelId, hasPlanningOverride, planningProvider, planningModelId, optionalSteps.length, enabledOptionalStepIds, priority, effectiveNodeId, projectId, addToast, submitTask]);
}, [description, submitting, selectedWorkflowId, dependencies, selectedAgentId, selectedPresetId, hasExecutorOverride, executorProvider, executorModelId, hasValidatorOverride, validatorProvider, validatorModelId, hasPlanningOverride, planningProvider, planningModelId, thinkingLevel, optionalSteps.length, enabledOptionalStepIds, priority, effectiveNodeId, projectId, addToast, submitTask]);
const handleDuplicateProceed = useCallback(async () => {
const matches = duplicateMatches;
@@ -660,6 +664,10 @@ export function InlineCreateCard({
setPlanningModelId(next.modelId);
}, []);
const handleThinkingLevelChange = useCallback((value: string) => {
setThinkingLevel(value);
}, []);
const handleToggleFavorite = useCallback(async (provider: string) => {
const currentFavorites = favoriteProviders;
const isFavorite = currentFavorites.includes(provider);
@@ -730,6 +738,7 @@ export function InlineCreateCard({
setValidatorModelId(undefined);
setPlanningProvider(undefined);
setPlanningModelId(undefined);
setThinkingLevel("");
setEnabledOptionalStepIds([]);
setSelectedPresetId(undefined);
setSelectedAgentId(null);
@@ -1218,6 +1227,9 @@ export function InlineCreateCard({
onExecutorChange={handleExecutorChange}
onValidatorChange={handleValidatorChange}
onPlanningChange={handlePlanningModelChange}
thinkingLevel={thinkingLevel}
onThinkingLevelChange={handleThinkingLevelChange}
defaultThinkingLevel={settings?.defaultThinkingLevel}
modelsLoading={modelsLoading}
modelsError={modelsError}
onRetry={loadModels}

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
import type { ModelPreset } from "@fusion/core";
import type { ModelPreset, ThinkingLevel } from "@fusion/core";
import type { ModelInfo } from "../api";
import { applyPresetToSelection } from "../utils/modelPresets";
import { CustomModelDropdown } from "./CustomModelDropdown";
@@ -20,6 +20,18 @@ interface ModelSelectionModalProps {
onExecutorChange: (value: string) => void;
onValidatorChange: (value: string) => void;
onPlanningChange?: (value: string) => void;
/** Current thinking-level override, or "" for the effective default. Optional so pre-existing callers keep compiling unchanged. */
thinkingLevel?: string;
/**
* FNXC:Settings-ThinkingLevel 2026-07-09-00:00:
* The quick-create "Select Models" dialog must expose the same thinking (reasoning-effort) levels — including `xhigh` —
* as the full task pickers (TaskForm, ModelSelectorTab), so a task created from the board carries the same override
* surface as one created from the New Task modal. The selector is rendered only when this handler is provided,
* mirroring the existing optional `onPlanningChange` pattern.
*/
onThinkingLevelChange?: (value: string) => void;
/** Effective default thinking level shown in the Default option, e.g. project/global `defaultThinkingLevel` (falls back to "off"). */
defaultThinkingLevel?: ThinkingLevel;
modelsLoading: boolean;
modelsError: string | null;
onRetry: () => void;
@@ -55,6 +67,9 @@ export function ModelSelectionModal({
onExecutorChange,
onValidatorChange,
onPlanningChange,
thinkingLevel = "",
onThinkingLevelChange,
defaultThinkingLevel,
modelsLoading,
modelsError,
onRetry,
@@ -310,6 +325,36 @@ 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>

View File

@@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
import { createPortal } from "react-dom";
import type { ToastType } from "../hooks/useToast";
import { DEFAULT_TASK_PRIORITY, TASK_PRIORITIES, getErrorMessage } from "@fusion/core";
import type { Task, Settings, TaskPriority, ResolvedWorkflowOptionalStep } from "@fusion/core";
import type { Task, Settings, TaskPriority, ResolvedWorkflowOptionalStep, ThinkingLevel } from "@fusion/core";
import type { ModelInfo, Agent, CreateTaskInput, DuplicateMatch, BoardWorkflowDefinition, NodeInfo } from "../api";
import { checkDuplicateTasks, fetchModels, fetchSettings, updateGlobalSettings, fetchAgents, uploadAttachment, fetchWorkflowOptionalSteps } from "../api";
import { DuplicateWarningModal } from "./DuplicateWarningModal";
@@ -170,13 +170,15 @@ 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" | null>(null);
const [activeModelSubmenu, setActiveModelSubmenu] = useState<"plan" | "executor" | "validator" | "thinking" | null>(null);
const [executorProvider, setExecutorProvider] = useState<string | undefined>(undefined);
const [executorModelId, setExecutorModelId] = useState<string | undefined>(undefined);
const [validatorProvider, setValidatorProvider] = useState<string | undefined>(undefined);
const [validatorModelId, setValidatorModelId] = useState<string | undefined>(undefined);
const [planningProvider, setPlanningProvider] = useState<string | undefined>(undefined);
const [planningModelId, setPlanningModelId] = useState<string | undefined>(undefined);
/* FNXC:Settings-ThinkingLevel 2026-07-09-00:00: inline quick-entry bar carries the same per-task thinking-level override as the full New Task modal; "" means "use default". */
const [thinkingLevel, setThinkingLevel] = useState<string>("");
const modelTriggerRef = useRef<HTMLButtonElement>(null);
const modelMenuPortalRef = useRef<HTMLDivElement>(null);
const agentPickerRef = useRef<HTMLDivElement>(null);
@@ -413,6 +415,7 @@ 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")
@@ -607,6 +610,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
setValidatorModelId(undefined);
setPlanningProvider(undefined);
setPlanningModelId(undefined);
setThinkingLevel("");
setSelectedPresetId(undefined);
setEnabledOptionalStepIds(optionalSteps.filter((step) => step.defaultOn).map((step) => step.templateId));
setIsFastMode(false);
@@ -731,6 +735,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
validatorModelId: hasValidatorOverride ? validatorModelId : undefined,
planningModelProvider: hasPlanningOverride ? planningProvider : undefined,
planningModelId: hasPlanningOverride ? planningModelId : undefined,
thinkingLevel: thinkingLevel !== "" ? (thinkingLevel as ThinkingLevel) : undefined,
/*
FNXC:QuickAddWorkflowSteps 2026-06-29-01:31:
Quick Add optional-step toggles are explicit task intent. When the workflow exposes optional steps and the user unchecks every one, submit an empty array instead of omitting the field so default-on Plan Review / Code Review do not reappear on the created task.
@@ -780,6 +785,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
hasPlanningOverride,
planningProvider,
planningModelId,
thinkingLevel,
enabledOptionalStepIds,
isFastMode,
settings,
@@ -1497,6 +1503,10 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
setValidatorModelId(next.modelId);
}, []);
const handleThinkingLevelChange = useCallback((value: string) => {
setThinkingLevel(value);
}, []);
const handleToggleFavorite = useCallback(async (provider: string) => {
// Delegate to parent callback when available
if (parentToggleFavorite) {
@@ -2341,6 +2351,59 @@ 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

View File

@@ -40,6 +40,8 @@ vi.mock("../ModelSelectionModal", () => ({
onExecutorChange,
onValidatorChange,
onPlanningChange,
thinkingLevel,
onThinkingLevelChange,
modelsLoading,
modelsError,
onRetry,
@@ -58,6 +60,8 @@ vi.mock("../ModelSelectionModal", () => ({
onExecutorChange: (value: string) => void;
onValidatorChange: (value: string) => void;
onPlanningChange: (value: string) => void;
thinkingLevel?: string;
onThinkingLevelChange?: (value: string) => void;
modelsLoading: boolean;
modelsError: string | null;
onRetry: () => void;
@@ -73,6 +77,7 @@ vi.mock("../ModelSelectionModal", () => ({
<div data-testid="modal-props-executor-value">{executorValue}</div>
<div data-testid="modal-props-validator-value">{validatorValue}</div>
<div data-testid="modal-props-planning-value">{planningValue}</div>
<div data-testid="modal-props-thinking-level">{thinkingLevel ?? ""}</div>
<div data-testid="modal-props-loading">{modelsLoading ? "loading" : "not-loading"}</div>
<div data-testid="modal-props-error">{modelsError || "no-error"}</div>
<div data-testid="modal-props-favorite-models">{JSON.stringify(favoriteModels ?? [])}</div>
@@ -87,6 +92,8 @@ vi.mock("../ModelSelectionModal", () => ({
<button data-testid="modal-clear-executor" onClick={() => onExecutorChange("")}>Clear Executor</button>
<button data-testid="modal-clear-validator" onClick={() => onValidatorChange("")}>Clear Reviewer</button>
<button data-testid="modal-clear-planning" onClick={() => onPlanningChange("")}>Clear Planning</button>
<button data-testid="modal-select-thinking" onClick={() => onThinkingLevelChange?.("high")}>Select Thinking</button>
<button data-testid="modal-clear-thinking" onClick={() => onThinkingLevelChange?.("")}>Clear Thinking</button>
<button data-testid="modal-retry" onClick={onRetry}>Retry</button>
</div>
);
@@ -612,6 +619,69 @@ describe("InlineCreateCard model selector", () => {
});
});
it("omits thinkingLevel from the submit payload when the thinking selector is left at default", async () => {
const { props } = renderCard();
expandCard();
const textarea = screen.getByPlaceholderText("What needs to be done?");
fireEvent.change(textarea, { target: { value: "Task using default thinking level" } });
fireEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => {
expect(props.onSubmit).toHaveBeenCalledWith(
expect.objectContaining({
description: "Task using default thinking level",
thinkingLevel: undefined,
}),
);
});
});
it("forwards a non-default thinkingLevel in the submit payload", async () => {
const { props } = renderCard();
expandCard();
const textarea = screen.getByPlaceholderText("What needs to be done?");
fireEvent.change(textarea, { target: { value: "Task with thinking level override" } });
openModelModal();
fireEvent.click(screen.getByTestId("modal-select-thinking"));
fireEvent.click(screen.getByTestId("modal-close"));
fireEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => {
expect(props.onSubmit).toHaveBeenCalledWith(
expect.objectContaining({
description: "Task with thinking level override",
thinkingLevel: "high",
}),
);
});
});
it("clears thinkingLevel back to undefined when reset to Default", async () => {
const { props } = renderCard();
expandCard();
const textarea = screen.getByPlaceholderText("What needs to be done?");
fireEvent.change(textarea, { target: { value: "Task clearing thinking level" } });
openModelModal();
fireEvent.click(screen.getByTestId("modal-select-thinking"));
expect(screen.getByTestId("modal-props-thinking-level").textContent).toBe("high");
fireEvent.click(screen.getByTestId("modal-clear-thinking"));
expect(screen.getByTestId("modal-props-thinking-level").textContent).toBe("");
fireEvent.click(screen.getByTestId("modal-close"));
fireEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => {
expect(props.onSubmit).toHaveBeenCalledWith(
expect.objectContaining({
description: "Task clearing thinking level",
thinkingLevel: undefined,
}),
);
});
});
it("does NOT call onCancel when focus leaves while the model modal is open", () => {
const { props } = renderCard();
expandCard();

View File

@@ -277,6 +277,75 @@ 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();
});
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;
expect(select).toBeTruthy();
const options = Array.from(select.options).map((o) => o.value);
expect(options).toEqual(["", "off", "minimal", "low", "medium", "high", "xhigh"]);
});
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();
});
it("still omits the thinking-level selector while models are loading", () => {
renderModelSelectionModal({ modelsLoading: true, onThinkingLevelChange: vi.fn() });
expect(screen.queryByTestId("model-selection-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();
});
it("calls onThinkingLevelChange with the selected level", () => {
const onThinkingLevelChange = vi.fn();
renderModelSelectionModal({ onThinkingLevelChange });
const select = screen.getByTestId("model-selection-thinking");
fireEvent.change(select, { target: { value: "high" } });
expect(onThinkingLevelChange).toHaveBeenCalledWith("high");
});
it("calls onThinkingLevelChange with '' when selecting the Default option", () => {
const onThinkingLevelChange = vi.fn();
renderModelSelectionModal({ onThinkingLevelChange, thinkingLevel: "high" });
const select = screen.getByTestId("model-selection-thinking");
fireEvent.change(select, { target: { value: "" } });
expect(onThinkingLevelChange).toHaveBeenCalledWith("");
});
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");
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");
expect(badge.textContent).toBe("xhigh");
expect(badge.classList.contains("model-badge-custom")).toBe(true);
});
});
describe("Preset selection", () => {
it("does not show preset selector when presets prop is omitted", () => {
renderModelSelectionModal();

View File

@@ -2587,6 +2587,39 @@ 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", () => {
renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Task with models" } });
openModelMenu();
expect(screen.getByTestId("model-menu-thinking")).toBeTruthy();
});
it("clicking Thinking opens a submenu with a level <select>", () => {
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"));
const select = screen.getByTestId("model-thinking-select") as HTMLSelectElement;
expect(select).toBeTruthy();
const options = Array.from(select.options).map((o) => o.value);
expect(options).toEqual(["", "off", "minimal", "low", "medium", "high", "xhigh"]);
expect(screen.getByTestId("model-submenu-back")).toBeTruthy();
});
it("clicking Executor opens submenu with CustomModelDropdown", () => {
renderQuickEntryBox({});
expandQuickEntry();
@@ -2969,6 +3002,48 @@ describe("QuickEntryBox", () => {
expect(addToast).not.toHaveBeenCalled();
});
it("forwards a non-default thinkingLevel selection in the submit payload", async () => {
const { props } = renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
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-submenu-back"));
fireEvent.keyDown(textarea, { key: "Escape" });
fireEvent.keyDown(textarea, { key: "Enter" });
await waitFor(() => {
expect(props.onCreate).toHaveBeenCalledWith(
expect.objectContaining({
description: "Task with thinking level override",
thinkingLevel: "xhigh",
}),
);
});
});
it("omits thinkingLevel from submit payload when left at default", async () => {
const { props } = renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Task without thinking override" } });
fireEvent.keyDown(textarea, { key: "Enter" });
await waitFor(() => {
expect(props.onCreate).toHaveBeenCalledWith(
expect.objectContaining({
description: "Task without thinking override",
thinkingLevel: undefined,
}),
);
});
});
it("includes all three selected model pairs in submit payload", async () => {
const { props } = renderQuickEntryBox({});
expandQuickEntry();

View File

@@ -4004,6 +4004,8 @@
"preset": "Preset",
"reviewerModel": "Reviewer Model",
"reviewerPlaceholder": "Select reviewer model…",
"thinkingDefault": "Default ({{level}})",
"thinkingModel": "Thinking",
"title": "Select Models",
"useDefault": "Use default",
"usingDefault": "Using default"

View File

@@ -3994,6 +3994,8 @@
"preset": "Predefinido",
"reviewerModel": "Modelo revisor",
"reviewerPlaceholder": "Seleccionar modelo revisor…",
"thinkingDefault": "",
"thinkingModel": "",
"title": "Seleccionar modelos",
"useDefault": "Usar predeterminado",
"usingDefault": "Usando predeterminado"

View File

@@ -3994,6 +3994,8 @@
"preset": "Préréglage",
"reviewerModel": "Modèle réviseur",
"reviewerPlaceholder": "Sélectionner le modèle réviseur…",
"thinkingDefault": "",
"thinkingModel": "",
"title": "Sélectionner les modèles",
"useDefault": "Utiliser par défaut",
"usingDefault": "Par défaut"

View File

@@ -3994,6 +3994,8 @@
"preset": "프리셋",
"reviewerModel": "검토자 모델",
"reviewerPlaceholder": "검토자 모델 선택…",
"thinkingDefault": "",
"thinkingModel": "",
"title": "모델 선택",
"useDefault": "기본값 사용",
"usingDefault": "기본값 사용 중"

View File

@@ -3994,6 +3994,8 @@
"preset": "预设",
"reviewerModel": "审查模型",
"reviewerPlaceholder": "选择审查模型…",
"thinkingDefault": "",
"thinkingModel": "",
"title": "选择模型",
"useDefault": "使用默认",
"usingDefault": "使用默认"

View File

@@ -3994,6 +3994,8 @@
"preset": "預設",
"reviewerModel": "審查模型",
"reviewerPlaceholder": "選擇審查模型…",
"thinkingDefault": "",
"thinkingModel": "",
"title": "選擇模型",
"useDefault": "使用預設",
"usingDefault": "使用預設"