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:
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user