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 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 18:04:42 +08:00
parent dd13d21c6a
commit cb2fe96a6e
3 changed files with 64 additions and 13 deletions

View File

@@ -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<HTMLDivElement>(null);
const stickToBottomRef = useRef(true);
const prevMessageCountRef = useRef(0);
@@ -76,8 +79,8 @@ export function MessageList() {
return (
<div className={styles.chat} ref={chatRef}>
{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";

View File

@@ -58,6 +58,7 @@ interface ChatContextValue {
extensionStatuses: Record<string, string>;
extensionDialog: ExtensionDialogState | null;
toolsExpanded: boolean;
showThinkingAndTools: boolean;
toggleSidebar: () => void;
closeSidebar: () => void;
loadSessions: () => Promise<void>;
@@ -77,6 +78,7 @@ interface ChatContextValue {
abortStream: () => Promise<void>;
abortRetry: () => Promise<void>;
toggleToolsExpanded: () => void;
toggleShowThinkingAndTools: () => void;
respondExtensionDialog: (response: Record<string, unknown>) => void;
dismissExtensionDialog: () => void;
applyModelSelection: (key: string) => Promise<void>;
@@ -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<Record<string, string>>({});
const [extensionDialog, setExtensionDialog] = useState<ExtensionDialogState | null>(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<string, SessionHistoryPayload>());
@@ -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<string, unknown>) => {
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,

View File

@@ -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<SettingsPaneId>("prompt");
const [systemPrompt, setSystemPrompt] = useState("");
const [systemPromptPath, setSystemPromptPath] = useState("");
@@ -1348,6 +1350,23 @@ export function SettingsPage() {
<p className={styles.fieldHint}>
http / https
</p>
<div className={styles.field}>
<span className={styles.fieldLabel}></span>
<label className={styles.skillToggle}>
<input
type="checkbox"
checked={showThinkingAndTools}
onChange={toggleShowThinkingAndTools}
/>
<span className={styles.skillToggleUi} aria-hidden />
<span className={styles.skillToggleLabel}>
{showThinkingAndTools ? "已开启" : "已关闭"}
</span>
</label>
<p className={styles.fieldHint}>
AI
</p>
</div>
</div>
{statusText && activePane === "other" ? (
<div className={`${styles.status} ${statusClass}`}>{statusText}</div>