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:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -3994,6 +3994,8 @@
|
||||
"preset": "Predefinido",
|
||||
"reviewerModel": "Modelo revisor",
|
||||
"reviewerPlaceholder": "Seleccionar modelo revisor…",
|
||||
"thinkingDefault": "",
|
||||
"thinkingModel": "",
|
||||
"title": "Seleccionar modelos",
|
||||
"useDefault": "Usar predeterminado",
|
||||
"usingDefault": "Usando predeterminado"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -3994,6 +3994,8 @@
|
||||
"preset": "프리셋",
|
||||
"reviewerModel": "검토자 모델",
|
||||
"reviewerPlaceholder": "검토자 모델 선택…",
|
||||
"thinkingDefault": "",
|
||||
"thinkingModel": "",
|
||||
"title": "모델 선택",
|
||||
"useDefault": "기본값 사용",
|
||||
"usingDefault": "기본값 사용 중"
|
||||
|
||||
@@ -3994,6 +3994,8 @@
|
||||
"preset": "预设",
|
||||
"reviewerModel": "审查模型",
|
||||
"reviewerPlaceholder": "选择审查模型…",
|
||||
"thinkingDefault": "",
|
||||
"thinkingModel": "",
|
||||
"title": "选择模型",
|
||||
"useDefault": "使用默认",
|
||||
"usingDefault": "使用默认"
|
||||
|
||||
@@ -3994,6 +3994,8 @@
|
||||
"preset": "預設",
|
||||
"reviewerModel": "審查模型",
|
||||
"reviewerPlaceholder": "選擇審查模型…",
|
||||
"thinkingDefault": "",
|
||||
"thinkingModel": "",
|
||||
"title": "選擇模型",
|
||||
"useDefault": "使用預設",
|
||||
"usingDefault": "使用預設"
|
||||
|
||||
Reference in New Issue
Block a user