diff --git a/core/http/react-ui/e2e/chat-composer.spec.js b/core/http/react-ui/e2e/chat-composer.spec.js
index 7e1b15238..fedd45a46 100644
--- a/core/http/react-ui/e2e/chat-composer.spec.js
+++ b/core/http/react-ui/e2e/chat-composer.spec.js
@@ -72,7 +72,7 @@ test.describe('Chat composer', () => {
await expect(list).toBeVisible()
await expect(list.locator('.dk-cmd-label')).toHaveText(['Chat', 'View'])
const names = await list.locator('.dk-cmd-name').allTextContents()
- expect(names).toEqual(['/model', '/new', '/chats', '/assistant', '/reasoning', '/effort', '/canvas', '/find', '/settings', '/export', '/clear'])
+ expect(names).toEqual(['/model', '/new', '/chats', '/assistant', '/effort', '/canvas', '/find', '/settings', '/export', '/clear'])
await input(page).fill('/set')
await expect(list.locator('.dk-cmd-name')).toHaveText(['/settings'])
@@ -149,7 +149,7 @@ test.describe('Chat composer', () => {
await openChat(page, [chatOf('c1', 'Empty', 'qwen3-8b', [])])
await input(page).fill('/')
const names = await page.getByRole('listbox', { name: 'Actions' }).locator('.dk-cmd-name').allTextContents()
- expect(names).toEqual(['/model', '/new', '/chats', '/assistant', '/reasoning', '/effort', '/canvas', '/settings'])
+ expect(names).toEqual(['/model', '/new', '/chats', '/assistant', '/effort', '/canvas', '/settings'])
})
test('"/" from outside a text field starts a command', async ({ page }) => {
diff --git a/core/http/react-ui/e2e/chat-reasoning.spec.js b/core/http/react-ui/e2e/chat-reasoning.spec.js
index ddd7a6324..d184f499f 100644
--- a/core/http/react-ui/e2e/chat-reasoning.spec.js
+++ b/core/http/react-ui/e2e/chat-reasoning.spec.js
@@ -1,8 +1,9 @@
import { test, expect } from './coverage-fixtures.js'
import { chatOf, pair, mockChat, openChat, storedChats } from './chat-fixtures.js'
-// The composer has two reasoning controls, stored per chat: an effort select
-// and a "No reasoning" toggle. Both are also slash actions.
+// The composer has one reasoning control, stored per chat: the effort select.
+// "None" asks for no reasoning and hides what the model still sends. It is
+// also the /effort slash action.
const sendWith = async (page, answer) => {
let body = null
@@ -40,12 +41,13 @@ test.describe('Chat reasoning controls', () => {
expect(body.reasoning_effort).toBe('high')
})
- test('the toggle asks for "none" and hides reasoning the model still sends', async ({ page }) => {
+ test('"None" asks for "none" and hides reasoning the model still sends', async ({ page }) => {
await openChat(page, [chatOf('c1', 'R', 'qwen3-8b', pair('hi', 'hello'))])
- const toggle = page.getByTestId('chat-reasoning-off')
- await toggle.click()
- await expect(toggle).toHaveAttribute('aria-pressed', 'true')
- await expect(page.getByTestId('chat-effort-chip')).toBeDisabled()
+ const chip = page.getByTestId('chat-effort-chip')
+ await chip.click()
+ await page.getByTestId('chat-effort-none').click()
+ await expect(chip).toContainText('None')
+ await expect(chip).toBeEnabled()
const body = await sendWith(page, stream({ reasoning: 'secret plan' }, { content: 'The answer.' }))
expect(body.reasoning_effort).toBe('none')
await expect(page.getByText('The answer.')).toBeVisible()
@@ -54,14 +56,17 @@ test.describe('Chat reasoning controls', () => {
expect(history.some(m => m.role === 'thinking')).toBe(false)
})
- test('the slash actions toggle reasoning and open the effort select', async ({ page }) => {
+ test('a chat saved with the old reasoningOff flag still hides reasoning', async ({ page }) => {
+ const chat = { ...chatOf('c1', 'R', 'qwen3-8b', pair('hi', 'hello')), reasoningOff: true }
+ await openChat(page, [chat])
+ await expect(page.getByTestId('chat-effort-chip')).toContainText('None')
+ const body = await sendWith(page, stream({ reasoning: 'secret plan' }, { content: 'The answer.' }))
+ expect(body.reasoning_effort).toBe('none')
+ await expect(page.getByText('secret plan')).toHaveCount(0)
+ })
+
+ test('the /effort slash action opens the effort select', async ({ page }) => {
await openChat(page, [chatOf('c1', 'R', 'qwen3-8b', pair('hi', 'hello'))])
- await page.getByTestId('chat-input').fill('/reasoning')
- await page.getByTestId('chat-input').press('Enter')
- await expect(page.getByTestId('chat-reasoning-off')).toHaveAttribute('aria-pressed', 'true')
- await page.getByTestId('chat-input').fill('/reasoning')
- await page.getByTestId('chat-input').press('Enter')
- await expect(page.getByTestId('chat-reasoning-off')).toHaveAttribute('aria-pressed', 'false')
await page.getByTestId('chat-input').fill('/effort')
await page.getByTestId('chat-input').press('Enter')
await expect(page.getByTestId('chat-effort-menu')).toBeVisible()
diff --git a/core/http/react-ui/public/locales/en/chat.json b/core/http/react-ui/public/locales/en/chat.json
index 6dfbed355..b714380ad 100644
--- a/core/http/react-ui/public/locales/en/chat.json
+++ b/core/http/react-ui/public/locales/en/chat.json
@@ -142,11 +142,7 @@
"low": "Low",
"medium": "Medium",
"high": "High"
- },
- "offLabel": "No reasoning",
- "offTitle": "Turn reasoning off for this chat: ask the model for no reasoning and hide any reasoning it sends",
- "offToast": "Reasoning is off for this chat",
- "onToast": "Reasoning is on for this chat"
+ }
},
"deleteAllDialog": {
"title": "Delete All Chats",
@@ -233,10 +229,6 @@
"label": "Reasoning effort",
"desc": "Choose how hard the model thinks"
},
- "reasoning": {
- "label": "Toggle reasoning",
- "desc": "Turn reasoning off or on for this chat"
- },
"assistant": {
"label": "Manage mode",
"desc": "Let this chat manage LocalAI"
diff --git a/core/http/react-ui/src/components/chat/ReasoningChips.jsx b/core/http/react-ui/src/components/chat/ReasoningChips.jsx
index cd6855169..cff9c96e4 100644
--- a/core/http/react-ui/src/components/chat/ReasoningChips.jsx
+++ b/core/http/react-ui/src/components/chat/ReasoningChips.jsx
@@ -3,26 +3,25 @@ import { useTranslation } from 'react-i18next'
import Icon from '../Icon'
import { REASONING_EFFORTS } from './chatActions'
-// The two reasoning controls in the Chat composer, both stored per chat:
-// the effort chip opens a menu. "Model default" sends no reasoning_effort.
-// the traces chip a toggle. On, the chat asks for reasoning_effort "none" and
-// the thread drops any reasoning the model still sends.
+// The reasoning effort chip in the Chat composer, stored per chat. It opens a
+// menu. "Model default" sends no reasoning_effort. "None" sends "none" and the
+// thread drops any reasoning the model still sends.
// The ref gives the page open() for the /effort action.
-const ReasoningChips = forwardRef(function ReasoningChips({ effort, off, onEffort, onOff }, ref) {
+const ReasoningChips = forwardRef(function ReasoningChips({ effort, onEffort }, ref) {
const { t } = useTranslation('chat')
const [open, setOpen] = useState(false)
const rootRef = useRef(null)
const chipRef = useRef(null)
const menuRef = useRef(null)
const choices = ['', ...REASONING_EFFORTS]
- const current = off ? 'none' : (effort || '')
+ const current = effort || ''
const nameOf = (v) => (v ? t(`reasoning.level.${v}`) : t('reasoning.default'))
const close = useCallback((restoreFocus = false) => {
setOpen(false)
if (restoreFocus) chipRef.current?.focus()
}, [])
- const openMenu = useCallback(() => { if (!off) setOpen(true) }, [off])
+ const openMenu = useCallback(() => setOpen(true), [])
useImperativeHandle(ref, () => ({ open: openMenu }), [openMenu])
useEffect(() => {
@@ -59,7 +58,6 @@ const ReasoningChips = forwardRef(function ReasoningChips({ effort, off, onEffor
className="home-chip"
aria-haspopup="menu"
aria-expanded={open}
- disabled={off}
title={t('reasoning.effortTitle')}
onClick={() => (open ? close() : openMenu())}
data-testid="chat-effort-chip"
@@ -94,17 +92,6 @@ const ReasoningChips = forwardRef(function ReasoningChips({ effort, off, onEffor
)}
-
)
})
diff --git a/core/http/react-ui/src/components/chat/chatActions.js b/core/http/react-ui/src/components/chat/chatActions.js
index 8e399800e..efc1b57f5 100644
--- a/core/http/react-ui/src/components/chat/chatActions.js
+++ b/core/http/react-ui/src/components/chat/chatActions.js
@@ -12,8 +12,6 @@ export const CHAT_SLASH_ACTIONS = [
{ id: 'new', cmd: '/new', icon: 'plus', group: 'chat' },
{ id: 'chats', cmd: '/chats', icon: 'history', group: 'chat' },
{ id: 'assistant', cmd: '/assistant', icon: 'user-shield', group: 'chat', needs: 'admin' },
- // /reasoning comes first so that Enter on "/reasoning" picks it, not "Reasoning effort".
- { id: 'reasoning', cmd: '/reasoning', icon: 'eye-off', group: 'chat' },
{ id: 'effort', cmd: '/effort', icon: 'brain', group: 'chat' },
{ id: 'canvas', cmd: '/canvas', icon: 'columns', group: 'view' },
{ id: 'find', cmd: '/find', icon: 'search', group: 'view', needs: 'thread' },
diff --git a/core/http/react-ui/src/hooks/useChat.js b/core/http/react-ui/src/hooks/useChat.js
index 292a28025..842d8a373 100644
--- a/core/http/react-ui/src/hooks/useChat.js
+++ b/core/http/react-ui/src/hooks/useChat.js
@@ -104,6 +104,11 @@ function loadChats() {
if (stored) {
const data = JSON.parse(stored)
if (data && Array.isArray(data.chats)) {
+ // Older chats stored a separate reasoningOff flag. It now means
+ // reasoning_effort 'none'.
+ data.chats = data.chats.map(({ reasoningOff, ...chat }) => (
+ reasoningOff ? { ...chat, reasoningEffort: 'none' } : chat
+ ))
return data
}
}
@@ -129,7 +134,6 @@ function saveChats(chats, activeChatId) {
topP: chat.topP,
topK: chat.topK,
reasoningEffort: chat.reasoningEffort,
- reasoningOff: chat.reasoningOff,
tokenUsage: chat.tokenUsage,
contextSize: chat.contextSize,
createdAt: chat.createdAt,
@@ -163,10 +167,9 @@ function createNewChat(model = '', systemPrompt = '', mcpMode = false) {
temperature: null,
topP: null,
topK: null,
- // '' leaves reasoning_effort to the model config. reasoningOff asks for
- // 'none' and hides reasoning traces in the thread.
+ // '' leaves reasoning_effort to the model config. 'none' also hides
+ // reasoning traces in the thread.
reasoningEffort: '',
- reasoningOff: false,
tokenUsage: { prompt: 0, completion: 0, total: 0 },
contextSize: null,
createdAt: Date.now(),
@@ -288,7 +291,7 @@ export function useChat(initialModel = '') {
const temperature = activeChat.temperature
const topP = activeChat.topP
const topK = activeChat.topK
- const hideReasoning = !!activeChat.reasoningOff
+ const hideReasoning = activeChat.reasoningEffort === 'none'
const showReasoning = (text) => { if (!hideReasoning) setStreamingReasoning(text) }
// Build user message content
@@ -383,7 +386,7 @@ export function useChat(initialModel = '') {
if (topP !== null && topP !== undefined) requestBody.top_p = topP
if (topK !== null && topK !== undefined) requestBody.top_k = topK
// reasoning_effort is honoured per request. 'none' also turns thinking off.
- const reasoningEffort = hideReasoning ? 'none' : activeChat.reasoningEffort
+ const reasoningEffort = activeChat.reasoningEffort
if (reasoningEffort) requestBody.reasoning_effort = reasoningEffort
// contextSize is the model's input+output window, not an
// output cap. Backends bound generation at remaining context
diff --git a/core/http/react-ui/src/pages/Chat.jsx b/core/http/react-ui/src/pages/Chat.jsx
index 98c90075a..ab420bc90 100644
--- a/core/http/react-ui/src/pages/Chat.jsx
+++ b/core/http/react-ui/src/pages/Chat.jsx
@@ -1002,11 +1002,6 @@ export default function Chat() {
/>
)
- const toggleReasoningOff = (reasoningOff) => {
- updateChatSettings(activeChat.id, { reasoningOff })
- addToast(t(reasoningOff ? 'reasoning.offToast' : 'reasoning.onToast'), 'info', 2000)
- }
-
const mcp = (
updateChatSettings(activeChat.id, { reasoningEffort })}
- onOff={toggleReasoningOff}
/>