From cb2fe96a6e57f778ba386ed4242d88d553bfa599 Mon Sep 17 00:00:00 2001 From: shumengya Date: Sun, 21 Jun 2026 18:04:42 +0800 Subject: [PATCH] feat: add toggle to show/hide thinking and tool call messages - Add showThinkingAndTools preference to ChatContext (default false, persisted in localStorage) - Filter thinking and tool_call messages in MessageList based on preference - Add toggle switch in Settings > Other panel Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/chat/MessageList.tsx | 9 ++-- frontend/src/context/ChatContext.tsx | 49 ++++++++++++++++---- frontend/src/routes/SettingsPage.tsx | 19 ++++++++ 3 files changed, 64 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/chat/MessageList.tsx b/frontend/src/components/chat/MessageList.tsx index 01b3eae..ca7553e 100644 --- a/frontend/src/components/chat/MessageList.tsx +++ b/frontend/src/components/chat/MessageList.tsx @@ -19,7 +19,10 @@ function isAgentSideRole(role: MessageRole): boolean { } export function MessageList() { - const { messages, toolsExpanded } = useChatContext(); + const { messages, toolsExpanded, showThinkingAndTools } = useChatContext(); + const visibleMessages = showThinkingAndTools + ? messages + : messages.filter((m) => m.role !== "thinking" && m.role !== "tool_call"); const chatRef = useRef(null); const stickToBottomRef = useRef(true); const prevMessageCountRef = useRef(0); @@ -76,8 +79,8 @@ export function MessageList() { return (
- {messages.map((msg, index) => { - const prev = messages[index - 1]; + {visibleMessages.map((msg, index) => { + const prev = visibleMessages[index - 1]; const prevIsAgentSide = prev ? isAgentSideRole(prev.role) : false; const isAgentSide = isAgentSideRole(msg.role); const agentGutter = isAgentSide ? (prevIsAgentSide ? "spacer" : "avatar") : "none"; diff --git a/frontend/src/context/ChatContext.tsx b/frontend/src/context/ChatContext.tsx index 8c4aded..b83879f 100644 --- a/frontend/src/context/ChatContext.tsx +++ b/frontend/src/context/ChatContext.tsx @@ -58,6 +58,7 @@ interface ChatContextValue { extensionStatuses: Record; extensionDialog: ExtensionDialogState | null; toolsExpanded: boolean; + showThinkingAndTools: boolean; toggleSidebar: () => void; closeSidebar: () => void; loadSessions: () => Promise; @@ -77,6 +78,7 @@ interface ChatContextValue { abortStream: () => Promise; abortRetry: () => Promise; toggleToolsExpanded: () => void; + toggleShowThinkingAndTools: () => void; respondExtensionDialog: (response: Record) => void; dismissExtensionDialog: () => void; applyModelSelection: (key: string) => Promise; @@ -90,6 +92,7 @@ const THINKING_LEVELS_LIST: ThinkingLevel[] = ["off", "minimal", "low", "medium" const LAST_ACTIVE_SESSION_KEY = "webui:lastActiveSessionPath"; const EPHEMERAL_SESSION_ID = "__ephemeral__"; const EPHEMERAL_CLEANUP_KEY = "webui:ephemeralSessionPath"; +const SHOW_THINKING_TOOLS_KEY = "webui:showThinkingAndTools"; function isActiveSessionBound( isEphemeral: boolean, @@ -512,6 +515,13 @@ export function ChatProvider({ children }: { children: ReactNode }) { const [extensionStatuses, setExtensionStatuses] = useState>({}); const [extensionDialog, setExtensionDialog] = useState(null); const [toolsExpanded, setToolsExpanded] = useState(false); + const [showThinkingAndTools, setShowThinkingAndTools] = useState(() => { + try { + return localStorage.getItem(SHOW_THINKING_TOOLS_KEY) === "true"; + } catch { + return false; + } + }); const [isEphemeralSession, setIsEphemeralSession] = useState(false); const sessionCache = useRef(new Map()); @@ -596,9 +606,16 @@ export function ChatProvider({ children }: { children: ReactNode }) { } const sid = data.sessionId != null ? String(data.sessionId) : ""; - if (data.sessionName && !isMachineSessionLabel(data.sessionName, sid)) { - setHeaderTitle(formatSessionTitle(data.sessionName)); - } + setSessions((currentSessions) => { + const pathKey = isEphemeralRef.current ? backendSessionPathRef.current : activeSessionPathRef.current; + const row = pathKey ? currentSessions.find((s) => s.path === pathKey) : undefined; + if (row?.name && !isMachineSessionLabel(row.name, sid)) { + setHeaderTitle(formatSessionTitle(row.name)); + } else if (data.sessionName && !isMachineSessionLabel(data.sessionName, sid)) { + setHeaderTitle(formatSessionTitle(data.sessionName)); + } + return currentSessions; + }); }); }); }, []); @@ -619,16 +636,14 @@ export function ChatProvider({ children }: { children: ReactNode }) { const pathKey = isEphemeralRef.current ? backendSessionPathRef.current : activeSessionPathRef.current; setSessions((currentSessions) => { const row = pathKey ? currentSessions.find((s) => s.path === pathKey) : undefined; - if (data.sessionName && !isMachineSessionLabel(data.sessionName, sid)) { + if (row?.name && !isMachineSessionLabel(row.name, sid)) { + setHeaderTitle(formatSessionTitle(row.name)); + } else if (data.sessionName && !isMachineSessionLabel(data.sessionName, sid)) { setHeaderTitle(formatSessionTitle(data.sessionName)); } else if (isEphemeralRef.current) { setHeaderTitle("临时对话"); - } else if (row) { - if (row.name && !isMachineSessionLabel(row.name, sid)) { - setHeaderTitle(formatSessionTitle(row.name)); - } else if (row.firstMessage && row.firstMessage !== "(空)") { - setHeaderTitle(formatSessionTitle(titleFromFirstUserMessage(row.firstMessage))); - } + } else if (row?.firstMessage && row.firstMessage !== "(空)") { + setHeaderTitle(formatSessionTitle(titleFromFirstUserMessage(row.firstMessage))); } return currentSessions; }); @@ -1360,6 +1375,18 @@ export function ChatProvider({ children }: { children: ReactNode }) { setToolsExpanded((prev) => !prev); }, []); + const toggleShowThinkingAndTools = useCallback(() => { + setShowThinkingAndTools((prev) => { + const next = !prev; + try { + localStorage.setItem(SHOW_THINKING_TOOLS_KEY, String(next)); + } catch { + /* ignore */ + } + return next; + }); + }, []); + const respondExtensionDialog = useCallback((response: Record) => { if (!extensionDialog) return; void chatApi.sendExtensionUiResponse(extensionDialog.id, response).catch(() => {}); @@ -1901,6 +1928,7 @@ export function ChatProvider({ children }: { children: ReactNode }) { extensionStatuses, extensionDialog, toolsExpanded, + showThinkingAndTools, toggleSidebar: () => setSidebarOpen((o) => !o), closeSidebar: () => setSidebarOpen(false), loadSessions, @@ -1916,6 +1944,7 @@ export function ChatProvider({ children }: { children: ReactNode }) { abortStream, abortRetry, toggleToolsExpanded, + toggleShowThinkingAndTools, respondExtensionDialog, dismissExtensionDialog, applyModelSelection, diff --git a/frontend/src/routes/SettingsPage.tsx b/frontend/src/routes/SettingsPage.tsx index 7be75cd..dba670b 100644 --- a/frontend/src/routes/SettingsPage.tsx +++ b/frontend/src/routes/SettingsPage.tsx @@ -5,6 +5,7 @@ import type { PromptFile } from "../api/prompts"; import * as settingsApi from "../api/settings"; import { useAvatars } from "../context/AvatarContext"; import { useBootstrap } from "../context/BootstrapContext"; +import { useChatContext } from "../context/ChatContext"; import type { AgentToolInfo, EnvironmentInfo, EnvironmentToolsInfo, ExtensionInfo, McpServerInfo, RuntimeInfo, SettingsPaneId, SkillInfo } from "../types/events"; import type { McpToolInfo } from "../types/events"; import { renderMarkdown } from "../utils/markdown"; @@ -309,6 +310,7 @@ function McpToolItem({ export function SettingsPage() { const { markRouteReady } = useBootstrap(); const { updateAvatars } = useAvatars(); + const { showThinkingAndTools, toggleShowThinkingAndTools } = useChatContext(); const [activePane, setActivePane] = useState("prompt"); const [systemPrompt, setSystemPrompt] = useState(""); const [systemPromptPath, setSystemPromptPath] = useState(""); @@ -1348,6 +1350,23 @@ export function SettingsPage() {

留空则不显示对应头像。仅支持 http / https 图片链接。

+
+ 显示思考过程与工具调用 + +

+ 开启后在对话中显示 AI 的思考过程和工具调用详情,默认关闭。 +

+
{statusText && activePane === "other" ? (
{statusText}