diff --git a/e2e/ai-provider-dropdown.spec.ts b/e2e/ai-provider-dropdown.spec.ts
index ea2c00bb2..adacbdd2c 100644
--- a/e2e/ai-provider-dropdown.spec.ts
+++ b/e2e/ai-provider-dropdown.spec.ts
@@ -339,4 +339,116 @@ test.describe("Settings → AI provider dropdown UX (B2)", () => {
.locator('button[type="submit"]'),
).toBeEnabled();
});
+
+ // #1126 — the Reasoning select on the Local and OpenAI-compatible forms
+ // saves with the form, and at phone width it sits in the same column as
+ // the model field without pushing the page sideways.
+ test("reasoning setting saves from the Local and gateway forms at 390 px", async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ const patchBodies: Array> = [];
+ let providerState: Record = {
+ provider: "LOCAL",
+ model: null,
+ baseUrl: "http://localhost:11434/v1",
+ responseTimeoutSeconds: null,
+ hasAnthropicKey: false,
+ anthropicKeyPreview: null,
+ hasLocalKey: false,
+ hasOpenaiKey: false,
+ openaiKeyPreview: null,
+ compatBaseUrl: "https://gateway.example.org/v1",
+ compatModel: "qwen3",
+ hasCompatKey: false,
+ localReasoningEffort: null,
+ compatReasoningEffort: null,
+ };
+ await page.route("**/api/insights/settings", (route) =>
+ route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ data: {
+ codexStatus: "disconnected",
+ codexConnectedAt: null,
+ hasAdminKey: false,
+ codexOauthConfigured: true,
+ privacyMode: "aggregated",
+ lastInsightAt: null,
+ },
+ error: null,
+ }),
+ }),
+ );
+ await page.route("**/api/user/ai-provider", async (route) => {
+ if (route.request().method() === "PATCH") {
+ const body = route.request().postDataJSON() as Record;
+ patchBodies.push(body);
+ providerState = { ...providerState, ...body };
+ }
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({ data: providerState, error: null }),
+ });
+ });
+ await page.route("**/api/insights/provider-chain", (route) =>
+ route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ data: {
+ activeProvider: "local",
+ cachedActiveProvider: null,
+ configuredChain: [
+ { providerType: "local", enabled: true, available: true },
+ ],
+ },
+ error: null,
+ }),
+ }),
+ );
+
+ const providerHydrated = page.waitForResponse("**/api/user/ai-provider");
+ await page.goto("/settings/ai?provider=local", {
+ waitUntil: "domcontentloaded",
+ });
+ await providerHydrated;
+
+ const localForm = page.getByTestId("ai-provider-config-local");
+ const localReasoning = page.locator("#ai-local-reasoning");
+ await expect(localReasoning).toHaveValue("");
+ // Same column, same width as the model select above it.
+ const [modelBox, reasoningBox] = await Promise.all([
+ page.locator("#ai-local-model").boundingBox(),
+ localReasoning.boundingBox(),
+ ]);
+ expect(reasoningBox?.x).toBe(modelBox?.x);
+ expect(reasoningBox?.width).toBe(modelBox?.width);
+ expect(
+ await page.evaluate(
+ () =>
+ document.documentElement.scrollWidth <=
+ document.documentElement.clientWidth,
+ ),
+ ).toBe(true);
+
+ await localReasoning.selectOption("none");
+ await localForm.locator('button[type="submit"]').click();
+ await expect.poll(() => patchBodies.length).toBe(1);
+ expect(patchBodies[0].localReasoningEffort).toBe("none");
+
+ await page
+ .getByTestId("ai-active-provider-select")
+ .selectOption("openai-compatible");
+ const compatForm = page.getByTestId("ai-provider-config-openai-compatible");
+ const compatReasoning = page.locator("#ai-compat-reasoning");
+ await expect(compatReasoning).toHaveValue("");
+ await compatReasoning.selectOption("high");
+ await compatForm.locator('button[type="submit"]').click();
+ await expect.poll(() => patchBodies.length).toBe(2);
+ expect(patchBodies[1].compatReasoningEffort).toBe("high");
+ expect(patchBodies[1]).not.toHaveProperty("localReasoningEffort");
+ });
});
diff --git a/messages/de.json b/messages/de.json
index 0c7173452..5186bb8b6 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -6080,6 +6080,17 @@
"title": "Coach-Pläne",
"description": "Die Pläne, die du mit dem Coach vereinbart hast, zum Lesen oder Entfernen.",
"open": "Pläne öffnen"
+ },
+ "reasoning": {
+ "label": "Nachdenken",
+ "hint": "Reasoning-Modelle wie Gemma oder Qwen denken nach, bevor sie antworten. Schaltest du es aus, bleibt das ganze Antwortbudget für die Antwort selbst.",
+ "options": {
+ "default": "Standard",
+ "none": "Aus",
+ "low": "Niedrig",
+ "medium": "Mittel",
+ "high": "Hoch"
+ }
}
},
"dashboard": {
diff --git a/messages/en.json b/messages/en.json
index 3cd9aa9a4..fc41483a9 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -6080,6 +6080,17 @@
"title": "Coach plans",
"description": "The plans you agreed with the Coach, to read or remove.",
"open": "Open plans"
+ },
+ "reasoning": {
+ "label": "Reasoning",
+ "hint": "Reasoning models such as Gemma or Qwen think before they answer. Off leaves the whole answer budget for the answer itself.",
+ "options": {
+ "default": "Default",
+ "none": "Off",
+ "low": "Low",
+ "medium": "Medium",
+ "high": "High"
+ }
}
},
"dashboard": {
diff --git a/messages/es.json b/messages/es.json
index d40131208..90919f624 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -6080,6 +6080,17 @@
"title": "Planes del Coach",
"description": "Los planes que acordaste con el Coach, para leerlos o eliminarlos.",
"open": "Abrir planes"
+ },
+ "reasoning": {
+ "label": "Razonamiento",
+ "hint": "Los modelos de razonamiento como Gemma o Qwen piensan antes de responder. Si lo desactivas, todo el presupuesto de respuesta se queda para la respuesta.",
+ "options": {
+ "default": "Predeterminado",
+ "none": "Desactivado",
+ "low": "Bajo",
+ "medium": "Medio",
+ "high": "Alto"
+ }
}
},
"dashboard": {
diff --git a/messages/fr.json b/messages/fr.json
index a5d0afa08..5d0c5570d 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -6080,6 +6080,17 @@
"title": "Plans du Coach",
"description": "Les plans convenus avec le Coach, à lire ou à supprimer.",
"open": "Ouvrir les plans"
+ },
+ "reasoning": {
+ "label": "Raisonnement",
+ "hint": "Les modèles de raisonnement comme Gemma ou Qwen réfléchissent avant de répondre. Désactivé, tout le budget de réponse sert à la réponse elle-même.",
+ "options": {
+ "default": "Par défaut",
+ "none": "Désactivé",
+ "low": "Faible",
+ "medium": "Moyen",
+ "high": "Élevé"
+ }
}
},
"dashboard": {
diff --git a/messages/it.json b/messages/it.json
index e0e489863..8654d8fea 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -6080,6 +6080,17 @@
"title": "Piani del Coach",
"description": "I piani concordati con il Coach, da leggere o rimuovere.",
"open": "Apri i piani"
+ },
+ "reasoning": {
+ "label": "Ragionamento",
+ "hint": "I modelli di ragionamento come Gemma o Qwen pensano prima di rispondere. Se lo disattivi, tutto il budget della risposta resta per la risposta stessa.",
+ "options": {
+ "default": "Predefinito",
+ "none": "Disattivato",
+ "low": "Basso",
+ "medium": "Medio",
+ "high": "Alto"
+ }
}
},
"dashboard": {
diff --git a/messages/ko.json b/messages/ko.json
index e51af7017..ac44f12e5 100644
--- a/messages/ko.json
+++ b/messages/ko.json
@@ -6080,6 +6080,17 @@
"title": "코치 계획",
"description": "코치와 합의한 계획을 읽거나 삭제할 수 있습니다.",
"open": "계획 열기"
+ },
+ "reasoning": {
+ "label": "추론",
+ "hint": "Gemma나 Qwen 같은 추론 모델은 답하기 전에 먼저 생각합니다. 끄면 답변 예산을 모두 답변 자체에 씁니다.",
+ "options": {
+ "default": "기본값",
+ "none": "끄기",
+ "low": "낮음",
+ "medium": "보통",
+ "high": "높음"
+ }
}
},
"dashboard": {
diff --git a/messages/pl.json b/messages/pl.json
index caa7f9245..17a2e68fe 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -6080,6 +6080,17 @@
"title": "Plany z Coachem",
"description": "Plany uzgodnione z Coachem, do przeczytania lub usunięcia.",
"open": "Otwórz plany"
+ },
+ "reasoning": {
+ "label": "Rozumowanie",
+ "hint": "Modele rozumujące, takie jak Gemma czy Qwen, myślą przed odpowiedzią. Po wyłączeniu cały budżet odpowiedzi idzie na samą odpowiedź.",
+ "options": {
+ "default": "Domyślnie",
+ "none": "Wyłączone",
+ "low": "Niskie",
+ "medium": "Średnie",
+ "high": "Wysokie"
+ }
}
},
"dashboard": {
diff --git a/src/components/settings/ai/__tests__/reasoning-effort-field.test.tsx b/src/components/settings/ai/__tests__/reasoning-effort-field.test.tsx
new file mode 100644
index 000000000..0f2f2efd2
--- /dev/null
+++ b/src/components/settings/ai/__tests__/reasoning-effort-field.test.tsx
@@ -0,0 +1,106 @@
+import { describe, expect, it } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+
+import { I18nProvider } from "@/lib/i18n/context";
+import en from "../../../../../messages/en.json";
+import { CompatProviderForm } from "../compat-provider-form";
+import { LocalProviderForm } from "../local-provider-form";
+import type { UserAIProvider } from "../shared";
+
+/**
+ * #1126 — the Reasoning select on the Local and OpenAI-compatible forms. It
+ * sits right under the model field, offers exactly Default / Off / Low /
+ * Medium / High, and seeds from the value the server stored on the entry.
+ * Saving it is pinned end to end in `e2e/ai-provider-dropdown.spec.ts`.
+ */
+
+function provider(overrides: Partial): UserAIProvider {
+ return {
+ provider: "LOCAL",
+ model: "llama3:8b",
+ baseUrl: "http://ollama.example.org:11434/v1",
+ hasAnthropicKey: false,
+ anthropicKeyPreview: null,
+ hasLocalKey: false,
+ hasOpenaiKey: false,
+ openaiKeyPreview: null,
+ compatBaseUrl: "https://gateway.example.org/v1",
+ compatModel: "qwen3",
+ hasCompatKey: false,
+ responseTimeoutSeconds: null,
+ ...overrides,
+ };
+}
+
+function render(node: React.ReactNode) {
+ const client = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+ });
+ return renderToStaticMarkup(
+
+ {node}
+ ,
+ );
+}
+
+/** The `
+
)}
+