diff --git a/CHANGELOG.md b/CHANGELOG.md
index f3c541684ea..1c1eab9ba8b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai
### Fixes
+- Control UI/chat: add a per-session thinking-level picker in the chat header and mobile chat settings, and keep the browser bundle on UI-local thinking/session-key helpers so Safari no longer crashes on Node-only imports before rendering chat controls.
- Synology Chat/security: route webhook token comparison through the shared constant-time secret helper for consistency with other bundled plugins.
- Gateway/security: scope loopback browser-origin auth throttling by normalized origin so one localhost Control UI tab cannot lock out a different localhost browser origin after repeated auth failures.
- Node exec approvals: keep node-host `system.run` approvals bound to the prepared execution plan, so script-drift revalidation still runs after agent-side approval forwarding.
diff --git a/ui/src/styles/chat/layout.css b/ui/src/styles/chat/layout.css
index 99bf65644bb..9455ee13495 100644
--- a/ui/src/styles/chat/layout.css
+++ b/ui/src/styles/chat/layout.css
@@ -743,8 +743,8 @@
}
.chat-controls__session {
- min-width: 140px;
- max-width: 300px;
+ min-width: 98px;
+ max-width: 190px;
}
.chat-controls__session-row {
@@ -755,8 +755,13 @@
}
.chat-controls__model {
- min-width: 170px;
- max-width: 320px;
+ min-width: 124px;
+ max-width: 206px;
+}
+
+.chat-controls__thinking-select {
+ min-width: 88px;
+ max-width: 118px;
}
.chat-controls__thinking {
@@ -781,13 +786,17 @@
.chat-controls__session select {
padding: 6px 10px;
font-size: 13px;
- max-width: 300px;
+ max-width: 190px;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-controls__model select {
- max-width: 320px;
+ max-width: 206px;
+}
+
+.chat-controls__thinking-select select {
+ max-width: 118px;
}
.chat-controls__thinking {
@@ -818,6 +827,11 @@
max-width: none;
}
+ .chat-controls__thinking-select {
+ min-width: 130px;
+ max-width: none;
+ }
+
.chat-controls {
gap: 8px;
}
@@ -863,6 +877,10 @@
.chat-controls__model {
min-width: 150px;
}
+
+ .chat-controls__thinking-select {
+ min-width: 140px;
+ }
}
/* Chat loading skeleton */
diff --git a/ui/src/ui/app-chat.ts b/ui/src/ui/app-chat.ts
index 637ba709236..9ace7542120 100644
--- a/ui/src/ui/app-chat.ts
+++ b/ui/src/ui/app-chat.ts
@@ -1,4 +1,3 @@
-import { parseAgentSessionKey } from "../../../src/sessions/session-key-utils.js";
import { scheduleChatScroll, resetChatScroll } from "./app-scroll.ts";
import { setLastActiveSessionKey } from "./app-settings.ts";
import { resetToolStream } from "./app-tool-stream.ts";
@@ -10,6 +9,7 @@ import { loadModels } from "./controllers/models.ts";
import { loadSessions } from "./controllers/sessions.ts";
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway.ts";
import { normalizeBasePath } from "./navigation.ts";
+import { parseAgentSessionKey } from "./session-key.ts";
import type { ChatModelOverride, ModelCatalogEntry } from "./types.ts";
import type { SessionsListResult } from "./types.ts";
import type { ChatAttachment, ChatQueueItem } from "./ui-types.ts";
diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts
index e85913dd46d..8a66aa4c2a1 100644
--- a/ui/src/ui/app-render.helpers.ts
+++ b/ui/src/ui/app-render.helpers.ts
@@ -1,6 +1,5 @@
import { html, nothing } from "lit";
import { repeat } from "lit/directives/repeat.js";
-import { parseAgentSessionKey } from "../../../src/sessions/session-key-utils.js";
import { t } from "../i18n/index.ts";
import { refreshChat } from "./app-chat.ts";
import { syncUrlWithSessionKey } from "./app-settings.ts";
@@ -16,8 +15,14 @@ import { ChatState, loadChatHistory } from "./controllers/chat.ts";
import { loadSessions } from "./controllers/sessions.ts";
import { icons } from "./icons.ts";
import { iconForTab, pathForTab, titleForTab, type Tab } from "./navigation.ts";
+import { parseAgentSessionKey } from "./session-key.ts";
import type { ThemeTransitionContext } from "./theme-transition.ts";
import type { ThemeMode, ThemeName } from "./theme.ts";
+import {
+ listThinkingLevelLabels,
+ normalizeThinkLevel,
+ resolveThinkingDefaultForModel,
+} from "./thinking.ts";
import type { SessionsListResult } from "./types.ts";
type SessionDefaultsSnapshot = {
@@ -133,11 +138,16 @@ function renderCronFilterIcon(hiddenCount: number) {
export function renderChatSessionSelect(state: AppViewState) {
const sessionGroups = resolveSessionOptionGroups(state, state.sessionKey, state.sessionsResult);
const modelSelect = renderChatModelSelect(state);
+ const thinkingSelect = renderChatThinkingSelect(state);
+ const selectedSessionLabel =
+ sessionGroups.flatMap((group) => group.options).find((entry) => entry.key === state.sessionKey)
+ ?.label ?? state.sessionKey;
return html`
- ${modelSelect}
+ ${modelSelect} ${thinkingSelect}
`;
}
@@ -436,6 +446,7 @@ export function renderChatMobileToggle(state: AppViewState) {
)}
+ ${renderChatThinkingSelect(state)}