diff --git a/ui/src/i18n/locales/en.ts b/ui/src/i18n/locales/en.ts
index 370fec9c660..2e0853ed079 100644
--- a/ui/src/i18n/locales/en.ts
+++ b/ui/src/i18n/locales/en.ts
@@ -161,6 +161,7 @@ export const en: TranslationMap = {
disconnected: "Disconnected from gateway.",
refreshTitle: "Refresh chat data",
thinkingToggle: "Toggle assistant thinking/working output",
+ toolCallsToggle: "Toggle tool calls and tool results",
focusToggle: "Toggle focus mode (hide sidebar + page header)",
hideCronSessions: "Hide cron sessions",
showCronSessions: "Show cron sessions",
diff --git a/ui/src/styles/chat/layout.css b/ui/src/styles/chat/layout.css
index 8b92c051fc1..2726d7041f6 100644
--- a/ui/src/styles/chat/layout.css
+++ b/ui/src/styles/chat/layout.css
@@ -990,3 +990,6 @@
background: var(--panel-strong);
border-color: var(--accent);
}
+
+/* Mobile dropdown toggle — hidden on desktop */
+/* Mobile gear toggle + dropdown are hidden by default in layout.css */
diff --git a/ui/src/styles/layout.css b/ui/src/styles/layout.css
index 6e19806bb32..ac87e1b106c 100644
--- a/ui/src/styles/layout.css
+++ b/ui/src/styles/layout.css
@@ -1030,3 +1030,16 @@
grid-template-columns: 1fr;
}
}
+
+/* Mobile chat controls — hidden on desktop, shown in layout.mobile.css */
+.chat-mobile-controls-wrapper {
+ display: none;
+}
+
+.chat-controls-mobile-toggle {
+ display: none;
+}
+
+.chat-controls-dropdown {
+ display: none;
+}
diff --git a/ui/src/styles/layout.mobile.css b/ui/src/styles/layout.mobile.css
index 036e6a7c588..cb5818190bd 100644
--- a/ui/src/styles/layout.mobile.css
+++ b/ui/src/styles/layout.mobile.css
@@ -316,23 +316,77 @@
display: none;
}
+ /* Hide the entire content-header on mobile chat — controls are in mobile gear menu */
.content--chat .content-header {
- display: flex;
- flex-direction: column;
- align-items: stretch;
- gap: 8px;
+ display: none;
}
.content--chat {
gap: 2px;
}
- .content--chat .content-header > div:first-child,
- .content--chat .page-meta,
- .content--chat .chat-controls {
+ /* Show the mobile gear toggle (lives in topbar now) */
+ .chat-mobile-controls-wrapper {
+ display: flex;
+ position: relative;
+ }
+
+ .chat-mobile-controls-wrapper .chat-controls-mobile-toggle {
+ display: flex;
+ }
+
+ /* The dropdown panel — anchored below the gear in topbar */
+ .chat-mobile-controls-wrapper .chat-controls-dropdown {
+ display: none;
+ position: absolute;
+ top: 100%;
+ right: 0;
+ z-index: 100;
+ background: var(--card, #161b22);
+ border: 1px solid var(--border, #30363d);
+ border-radius: 10px;
+ padding: 8px;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
+ flex-direction: column;
+ gap: 4px;
+ min-width: 220px;
+ }
+
+ .chat-mobile-controls-wrapper .chat-controls-dropdown.open {
+ display: flex;
+ }
+
+ .chat-mobile-controls-wrapper .chat-controls-dropdown .chat-controls {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
width: 100%;
}
+ .chat-mobile-controls-wrapper .chat-controls-dropdown .chat-controls__session {
+ min-width: unset;
+ max-width: unset;
+ width: 100%;
+ }
+
+ .chat-mobile-controls-wrapper .chat-controls-dropdown .chat-controls__session select {
+ width: 100%;
+ font-size: 14px;
+ padding: 10px 12px;
+ }
+
+ .chat-mobile-controls-wrapper .chat-controls-dropdown .chat-controls__thinking {
+ display: flex;
+ flex-direction: row;
+ gap: 6px;
+ padding: 4px 0;
+ justify-content: center;
+ }
+
+ .chat-mobile-controls-wrapper .chat-controls-dropdown .btn--icon {
+ min-width: 44px;
+ height: 44px;
+ }
.content {
padding: 4px 4px 16px;
gap: 12px;
diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts
index a7ecb15c370..77ba247a26d 100644
--- a/ui/src/ui/app-render.helpers.ts
+++ b/ui/src/ui/app-render.helpers.ts
@@ -173,7 +173,24 @@ export function renderChatControls(state: AppViewState) {
const disableThinkingToggle = state.onboarding;
const disableFocusToggle = state.onboarding;
const showThinking = state.onboarding ? false : state.settings.chatShowThinking;
+ const showToolCalls = state.onboarding ? true : state.settings.chatShowToolCalls;
const focusActive = state.onboarding ? true : state.settings.chatFocusMode;
+ const toolCallsIcon = html`
+
+ `;
const refreshIcon = html`
+ `;
+ const focusIcon = html`
+
+ `;
+
+ return html`
+
+
+
{
+ e.stopPropagation();
+ }}>
+
+
+
+
+
+
+
+
+
+
+ `;
+}
+
function switchChatSession(state: AppViewState, nextSessionKey: string) {
state.sessionKey = nextSessionKey;
state.chatMessage = "";
diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts
index 643edfca521..328f2cb6e33 100644
--- a/ui/src/ui/app-render.ts
+++ b/ui/src/ui/app-render.ts
@@ -8,6 +8,7 @@ import { refreshChatAvatar } from "./app-chat.ts";
import { renderUsageTab } from "./app-render-usage-tab.ts";
import {
renderChatControls,
+ renderChatMobileToggle,
renderChatSessionSelect,
renderTab,
renderSidebarConnectionStatus,
@@ -307,6 +308,7 @@ export function renderApp(state: AppViewState) {
const navDrawerOpen = Boolean(state.navDrawerOpen && !chatFocus && !state.onboarding);
const navCollapsed = Boolean(state.settings.navCollapsed && !navDrawerOpen);
const showThinking = state.onboarding ? false : state.settings.chatShowThinking;
+ const showToolCalls = state.onboarding ? true : state.settings.chatShowToolCalls;
const assistantAvatarUrl = resolveAssistantAvatarUrl(state);
const chatAvatarUrl = state.chatAvatarUrl ?? assistantAvatarUrl ?? null;
const configValue =
@@ -438,7 +440,10 @@ export function renderApp(state: AppViewState) {
${t("common.search")}
⌘K
- ${renderTopbarThemeModeToggle(state)}
+
+ ${isChat ? renderChatMobileToggle(state) : nothing}
+ ${renderTopbarThemeModeToggle(state)}
+
@@ -1346,6 +1351,7 @@ export function renderApp(state: AppViewState) {
},
thinkingLevel: state.chatThinkingLevel,
showThinking,
+ showToolCalls,
loading: state.chatLoading,
sending: state.chatSending,
compactionStatus: state.compactionStatus,
diff --git a/ui/src/ui/app-settings.test.ts b/ui/src/ui/app-settings.test.ts
index e259031d76e..aecc1f5bbcb 100644
--- a/ui/src/ui/app-settings.test.ts
+++ b/ui/src/ui/app-settings.test.ts
@@ -38,6 +38,7 @@ type SettingsHost = {
themeMode: ThemeMode;
chatFocusMode: boolean;
chatShowThinking: boolean;
+ chatShowToolCalls: boolean;
splitRatio: number;
navCollapsed: boolean;
navWidth: number;
@@ -95,6 +96,7 @@ const createHost = (tab: Tab): SettingsHost => ({
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
+ chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts
index 6b584be512b..5b7549c8d64 100644
--- a/ui/src/ui/chat/grouped-render.ts
+++ b/ui/src/ui/chat/grouped-render.ts
@@ -114,6 +114,7 @@ export function renderMessageGroup(
opts: {
onOpenSidebar?: (content: string) => void;
showReasoning: boolean;
+ showToolCalls?: boolean;
assistantName?: string;
assistantAvatar?: string | null;
basePath?: string;
@@ -165,6 +166,7 @@ export function renderMessageGroup(
{
isStreaming: group.isStreaming && index === group.messages.length - 1,
showReasoning: opts.showReasoning,
+ showToolCalls: opts.showToolCalls ?? true,
},
opts.onOpenSidebar,
),
@@ -619,7 +621,7 @@ function jsonSummaryLabel(parsed: unknown): string {
function renderGroupedMessage(
message: unknown,
- opts: { isStreaming: boolean; showReasoning: boolean },
+ opts: { isStreaming: boolean; showReasoning: boolean; showToolCalls?: boolean },
onOpenSidebar?: (content: string) => void,
) {
const m = message as Record;
@@ -632,7 +634,7 @@ function renderGroupedMessage(
typeof m.toolCallId === "string" ||
typeof m.tool_call_id === "string";
- const toolCards = extractToolCards(message);
+ const toolCards = (opts.showToolCalls ?? true) ? extractToolCards(message) : [];
const hasToolCards = toolCards.length > 0;
const images = extractImages(message);
const hasImages = images.length > 0;
@@ -656,7 +658,9 @@ function renderGroupedMessage(
return renderCollapsedToolCards(toolCards, onOpenSidebar);
}
- if (!markdown && !hasToolCards && !hasImages) {
+ // Suppress empty bubbles when tool cards are the only content and toggle is off
+ const visibleToolCards = hasToolCards && (opts.showToolCalls ?? true);
+ if (!markdown && !visibleToolCards && !hasImages) {
return nothing;
}
diff --git a/ui/src/ui/storage.node.test.ts b/ui/src/ui/storage.node.test.ts
index b3fc09f079d..64ce3aec95c 100644
--- a/ui/src/ui/storage.node.test.ts
+++ b/ui/src/ui/storage.node.test.ts
@@ -132,6 +132,7 @@ describe("loadSettings default gateway URL derivation", () => {
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
+ chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
@@ -157,6 +158,7 @@ describe("loadSettings default gateway URL derivation", () => {
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
+ chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
@@ -186,6 +188,7 @@ describe("loadSettings default gateway URL derivation", () => {
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
+ chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
@@ -202,6 +205,7 @@ describe("loadSettings default gateway URL derivation", () => {
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
+ chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
@@ -232,6 +236,7 @@ describe("loadSettings default gateway URL derivation", () => {
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
+ chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
@@ -250,6 +255,7 @@ describe("loadSettings default gateway URL derivation", () => {
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
+ chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
@@ -275,6 +281,7 @@ describe("loadSettings default gateway URL derivation", () => {
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
+ chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
@@ -289,6 +296,7 @@ describe("loadSettings default gateway URL derivation", () => {
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
+ chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
@@ -316,6 +324,7 @@ describe("loadSettings default gateway URL derivation", () => {
themeMode: "light",
chatFocusMode: false,
chatShowThinking: true,
+ chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 320,
diff --git a/ui/src/ui/storage.ts b/ui/src/ui/storage.ts
index 4a46b8d0703..02e826b3a1d 100644
--- a/ui/src/ui/storage.ts
+++ b/ui/src/ui/storage.ts
@@ -17,6 +17,7 @@ export type UiSettings = {
themeMode: ThemeMode;
chatFocusMode: boolean;
chatShowThinking: boolean;
+ chatShowToolCalls: boolean;
splitRatio: number; // Sidebar split ratio (0.4 to 0.7, default 0.6)
navCollapsed: boolean; // Collapsible sidebar state
navWidth: number; // Sidebar width when expanded (240–400px)
@@ -131,6 +132,7 @@ export function loadSettings(): UiSettings {
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
+ chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
@@ -173,6 +175,10 @@ export function loadSettings(): UiSettings {
typeof parsed.chatShowThinking === "boolean"
? parsed.chatShowThinking
: defaults.chatShowThinking,
+ chatShowToolCalls:
+ typeof parsed.chatShowToolCalls === "boolean"
+ ? parsed.chatShowToolCalls
+ : defaults.chatShowToolCalls,
splitRatio:
typeof parsed.splitRatio === "number" &&
parsed.splitRatio >= 0.4 &&
@@ -214,6 +220,7 @@ function persistSettings(next: UiSettings) {
themeMode: next.themeMode,
chatFocusMode: next.chatFocusMode,
chatShowThinking: next.chatShowThinking,
+ chatShowToolCalls: next.chatShowToolCalls,
splitRatio: next.splitRatio,
navCollapsed: next.navCollapsed,
navWidth: next.navWidth,
diff --git a/ui/src/ui/views/chat.browser.test.ts b/ui/src/ui/views/chat.browser.test.ts
index be2b5ab277e..fa7947a328a 100644
--- a/ui/src/ui/views/chat.browser.test.ts
+++ b/ui/src/ui/views/chat.browser.test.ts
@@ -9,6 +9,7 @@ function createProps(overrides: Partial = {}): ChatProps {
onSessionKeyChange: () => undefined,
thinkingLevel: null,
showThinking: false,
+ showToolCalls: true,
loading: false,
sending: false,
canAbort: false,
diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts
index 22c141c3919..860727c1927 100644
--- a/ui/src/ui/views/chat.test.ts
+++ b/ui/src/ui/views/chat.test.ts
@@ -123,6 +123,7 @@ function createProps(overrides: Partial = {}): ChatProps {
onSessionKeyChange: () => undefined,
thinkingLevel: null,
showThinking: false,
+ showToolCalls: true,
loading: false,
sending: false,
canAbort: false,
diff --git a/ui/src/ui/views/chat.ts b/ui/src/ui/views/chat.ts
index 1d0b877d042..88a712706f0 100644
--- a/ui/src/ui/views/chat.ts
+++ b/ui/src/ui/views/chat.ts
@@ -56,6 +56,7 @@ export type ChatProps = {
onSessionKeyChange: (next: string) => void;
thinkingLevel: string | null;
showThinking: boolean;
+ showToolCalls: boolean;
loading: boolean;
sending: boolean;
canAbort?: boolean;
@@ -932,6 +933,7 @@ export function renderChat(props: ChatProps) {
return renderMessageGroup(item, {
onOpenSidebar: props.onOpenSidebar,
showReasoning,
+ showToolCalls: props.showToolCalls,
assistantName: props.assistantName,
assistantAvatar: assistantIdentity.avatar,
basePath: props.basePath,
@@ -1409,7 +1411,7 @@ function buildChatItems(props: ChatProps): Array {
continue;
}
- if (!props.showThinking && normalized.role.toLowerCase() === "toolresult") {
+ if (!props.showToolCalls && normalized.role.toLowerCase() === "toolresult") {
continue;
}
@@ -1438,7 +1440,7 @@ function buildChatItems(props: ChatProps): Array {
startedAt: segments[i].ts,
});
}
- if (i < tools.length) {
+ if (i < tools.length && props.showToolCalls) {
items.push({
kind: "message",
key: messageKey(tools[i], i + history.length),