diff --git a/.changeset/thinking-level-selector-parity.md b/.changeset/thinking-level-selector-parity.md
new file mode 100644
index 0000000000..4546c90851
--- /dev/null
+++ b/.changeset/thinking-level-selector-parity.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": minor
+---
+
+summary: Add thinking-level controls to agent and bulk task model selectors.
+category: feature
+dev: Dashboard agent detail/onboarding model pickers and List bulk model updates now persist thinkingLevel.
diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md
index b4e1c535ff..3c6f868e24 100644
--- a/docs/dashboard-guide.md
+++ b/docs/dashboard-guide.md
@@ -268,7 +268,7 @@ Features:
- Sectioned task table grouped by lifecycle column
- Sortable columns (ID/title/status/column)
- Column visibility toggles and optional hide-done filtering
-- Bulk selection + batch model updates
+- Bulk selection + batch model, node, and task thinking-level updates
- Bulk Pause / Unpause / Archive actions from the selection toolbar (`Pause selected`, `Unpause selected`, `Archive selected`) for fast batch task state management.
- Bulk delete from the selection toolbar (`Delete selected`): archived selections are skipped automatically, and dependency-conflict failures can be force-deleted per task after a danger confirmation that removes dependency references.
- Desktop List view keeps the two-pane table/detail split. Tablet-width and mobile viewports use the single-pane card layout so list controls and quick-add stay full-width; tapping a task opens detail instead of selecting an embedded split pane.
@@ -500,7 +500,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**, 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, Agent Onboarding review, and Agent Detail built-in-model settings are concrete-only and start/fall back to **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.
@@ -994,6 +994,7 @@ Features:
- First-run setup asks whether to create an optional project agent after project registration. The default template is **CEO**; users can choose another preset, use the AI interview when `experimentalFeatures.agentOnboarding` is enabled, or skip it. Fusion can still build tasks without an agent by starting temporary agents to plan, code, review, and merge task work.
- Start, pause, stop, and trigger agent runs from the view and from detail panels
- In **Agent detail**, use the kebab **Bulk agent actions** button in the header utility cluster (next to **Refresh** and **Close**) to run project-wide lifecycle transitions for non-ephemeral agents in the current project — **Pause All Agents** targets agents in the `active` or `running` state, while **Resume All Agents** targets agents in the `paused` state only
+- In **Agent detail → Settings → Configuration**, the built-in-model picker includes a concrete **Thinking Level** selector; changing it autosaves to the agent's `runtimeConfig.thinkingLevel` alongside the provider/model choice.
- Bulk menu items stay disabled when nothing is eligible and show an inline hint (`Loading eligible agents...`, `No active agents eligible`, `No paused agents eligible`, or the current eligible count such as `Pause 2 active/running agents`)
- Bulk lifecycle flow: open **Bulk agent actions**, review the eligibility hint, confirm the modal, then use the success or partial-failure toast to verify paused/resumed counts plus skipped/failed agents
- Open agent detail tabs for runs, logs, read-only mail (agent inbox/outbox), settings/config, tasks, memory, and chain-of-command relationships
diff --git a/packages/dashboard/app/api/legacy.ts b/packages/dashboard/app/api/legacy.ts
index 25db4c998b..f5a03f1c22 100644
--- a/packages/dashboard/app/api/legacy.ts
+++ b/packages/dashboard/app/api/legacy.ts
@@ -602,6 +602,7 @@ export function updateTask(
* @param modelId - Executor model ID (optional, null to clear)
* @param validatorModelProvider - Validator model provider (optional, null to clear)
* @param validatorModelId - Validator model ID (optional, null to clear)
+ * @param thinkingLevel - Executor thinking level (optional, null to clear)
* @returns Promise with updated tasks and count
*/
export function batchUpdateTaskModels(
@@ -613,6 +614,7 @@ export function batchUpdateTaskModels(
planningModelProvider?: string | null,
planningModelId?: string | null,
nodeId?: string | null,
+ thinkingLevel?: string | null,
projectId?: string,
): Promise<{ updated: Task[]; count: number }> {
return api<{ updated: Task[]; count: number }>(withProjectId("/tasks/batch-update-models", projectId), {
@@ -626,6 +628,7 @@ export function batchUpdateTaskModels(
planningModelProvider,
planningModelId,
nodeId,
+ ...(thinkingLevel !== undefined ? { thinkingLevel } : {}),
}),
});
}
diff --git a/packages/dashboard/app/components/AgentDetailView.tsx b/packages/dashboard/app/components/AgentDetailView.tsx
index bcfa62c2ca..edd2e495a5 100644
--- a/packages/dashboard/app/components/AgentDetailView.tsx
+++ b/packages/dashboard/app/components/AgentDetailView.tsx
@@ -15,7 +15,7 @@ import remarkGfm from "remark-gfm";
import type { AgentDetail, AgentState, AgentHeartbeatRun, AgentBudgetStatus, ModelInfo, MemoryFileInfo, AgentCapability, PluginRuntimeInfo, SkillContent, AgentOnboardingSummary, AgentMailboxResponse, AgentPromptSizePoint } from "../api";
import { fetchAgent, updateAgent, updateAgentState, deleteAgent, fetchAgentLogsWithMeta, fetchAgentRunLogs, fetchAgentChildren, fetchAgentRuns, fetchAgentRunDetail, startAgentRun, stopAgentRun, updateAgentInstructions, updateAgentSoul, updateAgentMemory, fetchAgentMemoryFiles, fetchAgentMemoryFile, saveAgentMemoryFile, fetchAgentTasks, fetchChainOfCommand, fetchAgentBudgetStatus, resetAgentBudget, fetchWorkspaceFileContent, saveWorkspaceFileContent, fetchModels, fetchPluginRuntimes, fetchAgents, fetchSettingsByScope, upgradeAgentHeartbeatProcedure, fetchSkillContent, uploadAgentAvatar, deleteAgentAvatar, fetchAgentMailbox, markMessageRead, fetchAgentPromptSizes } from "../api";
import type { Agent } from "../api";
-import type { AgentLogEntry, Task, Message, ParticipantType, AgentPermissionPolicy, AgentPermissionPolicyRules, AgentPermission } from "@fusion/core";
+import type { AgentLogEntry, Task, Message, ParticipantType, AgentPermissionPolicy, AgentPermissionPolicyRules, AgentPermission, ThinkingLevel } from "@fusion/core";
import { AGENT_PERMISSIONS, getErrorMessage, isEphemeralAgent } from "@fusion/core";
import { AgentLogViewer } from "./AgentLogViewer";
import { LoadingSpinner } from "./LoadingSpinner";
@@ -3709,6 +3709,7 @@ function ConfigTab({
initial[field.key] = String(raw);
}
}
+ initial.thinkingLevel = typeof agent.runtimeConfig?.thinkingLevel === "string" ? agent.runtimeConfig.thinkingLevel : "off";
return initial;
});
@@ -4084,6 +4085,7 @@ function ConfigTab({
if (runtimeMode !== (initialRuntimeHint ? "runtime" : "model")) return true;
if (modelValue !== initialModelValue) return true;
if (selectedRuntimeId !== initialRuntimeHint) return true;
+ if ((formValues.thinkingLevel || "off") !== (typeof rc.thinkingLevel === "string" ? rc.thinkingLevel : "off")) return true;
return false;
})();
@@ -4130,6 +4132,10 @@ function ConfigTab({
setSkipHeartbeatWhenIdle(deriveSkipHeartbeatWhenIdle(agent.runtimeConfig));
setHeartbeatScopeDiscipline(deriveHeartbeatScopeDiscipline(agent.runtimeConfig));
setBudgetValues(deriveBudgetValues(agent.runtimeConfig));
+ setFormValues((prev) => ({
+ ...prev,
+ thinkingLevel: typeof agent.runtimeConfig?.thinkingLevel === "string" ? agent.runtimeConfig.thinkingLevel : "off",
+ }));
setModelValue(initialModelValue);
setSelectedRuntimeId(initialRuntimeHint);
setRuntimeMode(initialRuntimeHint ? "runtime" : "model");
@@ -4321,6 +4327,9 @@ function ConfigTab({
newRuntimeConfig.heartbeatPromptTemplate = heartbeatPromptTemplate;
}
+ const selectedThinkingLevel = (formValues.thinkingLevel || "off") as ThinkingLevel;
+ newRuntimeConfig.thinkingLevel = selectedThinkingLevel;
+
if (runtimeMode === "runtime") {
if (selectedRuntimeId.trim()) {
newRuntimeConfig.runtimeHint = selectedRuntimeId.trim();
@@ -4723,6 +4732,10 @@ function ConfigTab({
{runtimeMode === "model" ? (
+ {/*
+ FNXC:Settings-ThinkingLevel 2026-07-12-00:00:
+ Agent Detail now lets operators change a built-in agent's persisted runtimeConfig.thinkingLevel after creation through the shared inline model-dropdown control, matching NewAgentDialog's concrete-only agent semantics.
+ */}
{
+ setFormValues((prev) => ({ ...prev, thinkingLevel: level as ThinkingLevel }));
+ void scheduleAutoSave();
+ }}
/>
) : (
diff --git a/packages/dashboard/app/components/AgentOnboardingModal.tsx b/packages/dashboard/app/components/AgentOnboardingModal.tsx
index b268728d26..4b359a9f4f 100644
--- a/packages/dashboard/app/components/AgentOnboardingModal.tsx
+++ b/packages/dashboard/app/components/AgentOnboardingModal.tsx
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
+import type { ThinkingLevel } from "@fusion/core";
import type { AgentCapability, ConversationHistoryEntry } from "../api";
import {
startAgentOnboardingStreaming,
@@ -227,8 +228,6 @@ export function AgentOnboardingModal({ isOpen, onClose, onCreated, addToast, pro
{t("agents.name", "Name")}: {summary.name}
{t("agents.role", "Role")}: {summary.role}
-
- {}} readOnly />
{}} readOnly />
@@ -239,6 +238,10 @@ export function AgentOnboardingModal({ isOpen, onClose, onCreated, addToast, pro
{runtimeMode === "model" && (
<>
+ {/*
+ FNXC:Settings-ThinkingLevel 2026-07-12-00:00:
+ Agent onboarding's model picker owns the generated runtimeConfig.thinkingLevel so operators can edit the concrete reasoning effort before creating the agent, matching NewAgentDialog without an inherit/default lane.
+ */}
{
+ setSummary((current) => current ? { ...current, thinkingLevel: level as ThinkingLevel } : current);
+ }}
/>
>
)}
diff --git a/packages/dashboard/app/components/ListView.tsx b/packages/dashboard/app/components/ListView.tsx
index 461381612d..4431c85ea2 100644
--- a/packages/dashboard/app/components/ListView.tsx
+++ b/packages/dashboard/app/components/ListView.tsx
@@ -4,8 +4,8 @@ import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
import { ArrowUpDown, ArrowUp, ArrowDown, Link, Columns3, EyeOff, Eye, ChevronRight, Zap, Trash2, Pause, Play, Archive } from "lucide-react";
-import type { Task, TaskDetail, Column, ColumnId, TaskCreateInput, MergeResult, GithubIssueAction, PrInfo } from "@fusion/core";
-import { COLUMNS, DEFAULT_COLUMN, getErrorMessage, isColumn } from "@fusion/core";
+import type { Task, TaskDetail, Column, ColumnId, TaskCreateInput, MergeResult, GithubIssueAction, PrInfo, ThinkingLevel } from "@fusion/core";
+import { COLUMNS, DEFAULT_COLUMN, THINKING_LEVELS, getErrorMessage, isColumn } from "@fusion/core";
import { resolveEffectiveAutoMerge } from "../../../core/src/task-merge";
import { useColumnLabel } from "../i18n/labels";
import { sortTasksForDisplayColumn } from "./taskSorting";
@@ -568,10 +568,20 @@ export function ListView({
};
}, [projectId, useSinglePaneList]);
+ // Bulk edit state and handlers (declared before clearSelection so every clear path resets pending lane edits)
+ const [executorModel, setExecutorModel] = useState("__no_change__");
+ const [validatorModel, setValidatorModel] = useState("__no_change__");
+ const [bulkThinkingLevel, setBulkThinkingLevel] = useState("__no_change__");
+ const [nodeOverride, setNodeOverride] = useState("__no_change__");
+
const toggleBulkEdit = useCallback(() => {
setBulkEditEnabled((prev) => {
if (prev) {
setSelectedTaskIds(new Set());
+ setExecutorModel("__no_change__");
+ setValidatorModel("__no_change__");
+ setBulkThinkingLevel("__no_change__");
+ setNodeOverride("__no_change__");
}
return !prev;
});
@@ -593,6 +603,10 @@ export function ListView({
// Clear selection
const clearSelection = useCallback(() => {
setSelectedTaskIds(new Set());
+ setExecutorModel("__no_change__");
+ setValidatorModel("__no_change__");
+ setBulkThinkingLevel("__no_change__");
+ setNodeOverride("__no_change__");
}, []);
// Toggle a column's visibility
@@ -932,9 +946,6 @@ export function ListView({
}, [groupedTasks, isArchivedColumn, selectedTaskIds]);
// Bulk edit state and handlers (must be after groupedTasks and clearSelection definition)
- const [executorModel, setExecutorModel] = useState("__no_change__");
- const [validatorModel, setValidatorModel] = useState("__no_change__");
- const [nodeOverride, setNodeOverride] = useState("__no_change__");
const [availableNodes, setAvailableNodes] = useState([]);
const [isLoadingNodes, setIsLoadingNodes] = useState(false);
const selectedOverrideNode = useMemo(
@@ -1388,6 +1399,7 @@ export function ListView({
validatorModelProvider?: string | null;
validatorModelId?: string | null;
nodeId?: string | null;
+ thinkingLevel?: ThinkingLevel | null;
} = { taskIds };
if (executorModel !== "__no_change__") {
@@ -1426,6 +1438,10 @@ export function ListView({
}
}
+ if (bulkThinkingLevel !== "__no_change__") {
+ payload.thinkingLevel = bulkThinkingLevel === "" ? null : bulkThinkingLevel as ThinkingLevel;
+ }
+
// Check if any changes were made
if (Object.keys(payload).length === 1) {
addToast(t("listView.bulkNoChanges", "No changes to apply"), "info");
@@ -1443,6 +1459,7 @@ export function ListView({
undefined,
undefined,
payload.nodeId,
+ payload.thinkingLevel,
projectId,
);
@@ -1456,13 +1473,14 @@ export function ListView({
clearSelection();
setExecutorModel("__no_change__");
setValidatorModel("__no_change__");
+ setBulkThinkingLevel("__no_change__");
setNodeOverride("__no_change__");
} catch (err) {
addToast(getErrorMessage(err) || t("listView.bulkUpdateFailed", "Failed to update models"), "error");
} finally {
setIsApplying(false);
}
- }, [selectedTaskIds, tasks, executorModel, validatorModel, nodeOverride, projectId, addToast, clearSelection, isArchivedColumn, onTasksUpdated]);
+ }, [selectedTaskIds, tasks, executorModel, validatorModel, bulkThinkingLevel, nodeOverride, projectId, addToast, clearSelection, isArchivedColumn, onTasksUpdated]);
const closeContextMenu = useCallback(() => {
setContextMenuState(null);
@@ -2345,7 +2363,7 @@ export function ListView({
+ {/*
+ FNXC:Settings-ThinkingLevel 2026-07-12-00:00:
+ List bulk edit needs a no-change sentinel plus a clear-to-default lane for task.thinkingLevel so operators can update reasoning effort independently from executor/reviewer model overrides.
+ */}
+
+