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() {
|
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 chatRef = useRef<HTMLDivElement>(null);
|
||||||
const stickToBottomRef = useRef(true);
|
const stickToBottomRef = useRef(true);
|
||||||
const prevMessageCountRef = useRef(0);
|
const prevMessageCountRef = useRef(0);
|
||||||
@@ -76,8 +79,8 @@ export function MessageList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.chat} ref={chatRef}>
|
<div className={styles.chat} ref={chatRef}>
|
||||||
{messages.map((msg, index) => {
|
{visibleMessages.map((msg, index) => {
|
||||||
const prev = messages[index - 1];
|
const prev = visibleMessages[index - 1];
|
||||||
const prevIsAgentSide = prev ? isAgentSideRole(prev.role) : false;
|
const prevIsAgentSide = prev ? isAgentSideRole(prev.role) : false;
|
||||||
const isAgentSide = isAgentSideRole(msg.role);
|
const isAgentSide = isAgentSideRole(msg.role);
|
||||||
const agentGutter = isAgentSide ? (prevIsAgentSide ? "spacer" : "avatar") : "none";
|
const agentGutter = isAgentSide ? (prevIsAgentSide ? "spacer" : "avatar") : "none";
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ interface ChatContextValue {
|
|||||||
extensionStatuses: Record<string, string>;
|
extensionStatuses: Record<string, string>;
|
||||||
extensionDialog: ExtensionDialogState | null;
|
extensionDialog: ExtensionDialogState | null;
|
||||||
toolsExpanded: boolean;
|
toolsExpanded: boolean;
|
||||||
|
showThinkingAndTools: boolean;
|
||||||
toggleSidebar: () => void;
|
toggleSidebar: () => void;
|
||||||
closeSidebar: () => void;
|
closeSidebar: () => void;
|
||||||
loadSessions: () => Promise<void>;
|
loadSessions: () => Promise<void>;
|
||||||
@@ -77,6 +78,7 @@ interface ChatContextValue {
|
|||||||
abortStream: () => Promise<void>;
|
abortStream: () => Promise<void>;
|
||||||
abortRetry: () => Promise<void>;
|
abortRetry: () => Promise<void>;
|
||||||
toggleToolsExpanded: () => void;
|
toggleToolsExpanded: () => void;
|
||||||
|
toggleShowThinkingAndTools: () => void;
|
||||||
respondExtensionDialog: (response: Record<string, unknown>) => void;
|
respondExtensionDialog: (response: Record<string, unknown>) => void;
|
||||||
dismissExtensionDialog: () => void;
|
dismissExtensionDialog: () => void;
|
||||||
applyModelSelection: (key: string) => Promise<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 LAST_ACTIVE_SESSION_KEY = "webui:lastActiveSessionPath";
|
||||||
const EPHEMERAL_SESSION_ID = "__ephemeral__";
|
const EPHEMERAL_SESSION_ID = "__ephemeral__";
|
||||||
const EPHEMERAL_CLEANUP_KEY = "webui:ephemeralSessionPath";
|
const EPHEMERAL_CLEANUP_KEY = "webui:ephemeralSessionPath";
|
||||||
|
const SHOW_THINKING_TOOLS_KEY = "webui:showThinkingAndTools";
|
||||||
|
|
||||||
function isActiveSessionBound(
|
function isActiveSessionBound(
|
||||||
isEphemeral: boolean,
|
isEphemeral: boolean,
|
||||||
@@ -512,6 +515,13 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||||||
const [extensionStatuses, setExtensionStatuses] = useState<Record<string, string>>({});
|
const [extensionStatuses, setExtensionStatuses] = useState<Record<string, string>>({});
|
||||||
const [extensionDialog, setExtensionDialog] = useState<ExtensionDialogState | null>(null);
|
const [extensionDialog, setExtensionDialog] = useState<ExtensionDialogState | null>(null);
|
||||||
const [toolsExpanded, setToolsExpanded] = useState(false);
|
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 [isEphemeralSession, setIsEphemeralSession] = useState(false);
|
||||||
|
|
||||||
const sessionCache = useRef(new Map<string, SessionHistoryPayload>());
|
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) : "";
|
const sid = data.sessionId != null ? String(data.sessionId) : "";
|
||||||
if (data.sessionName && !isMachineSessionLabel(data.sessionName, sid)) {
|
setSessions((currentSessions) => {
|
||||||
setHeaderTitle(formatSessionTitle(data.sessionName));
|
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;
|
const pathKey = isEphemeralRef.current ? backendSessionPathRef.current : activeSessionPathRef.current;
|
||||||
setSessions((currentSessions) => {
|
setSessions((currentSessions) => {
|
||||||
const row = pathKey ? currentSessions.find((s) => s.path === pathKey) : undefined;
|
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));
|
setHeaderTitle(formatSessionTitle(data.sessionName));
|
||||||
} else if (isEphemeralRef.current) {
|
} else if (isEphemeralRef.current) {
|
||||||
setHeaderTitle("临时对话");
|
setHeaderTitle("临时对话");
|
||||||
} else if (row) {
|
} else if (row?.firstMessage && row.firstMessage !== "(空)") {
|
||||||
if (row.name && !isMachineSessionLabel(row.name, sid)) {
|
setHeaderTitle(formatSessionTitle(titleFromFirstUserMessage(row.firstMessage)));
|
||||||
setHeaderTitle(formatSessionTitle(row.name));
|
|
||||||
} else if (row.firstMessage && row.firstMessage !== "(空)") {
|
|
||||||
setHeaderTitle(formatSessionTitle(titleFromFirstUserMessage(row.firstMessage)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return currentSessions;
|
return currentSessions;
|
||||||
});
|
});
|
||||||
@@ -1360,6 +1375,18 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||||||
setToolsExpanded((prev) => !prev);
|
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>) => {
|
const respondExtensionDialog = useCallback((response: Record<string, unknown>) => {
|
||||||
if (!extensionDialog) return;
|
if (!extensionDialog) return;
|
||||||
void chatApi.sendExtensionUiResponse(extensionDialog.id, response).catch(() => {});
|
void chatApi.sendExtensionUiResponse(extensionDialog.id, response).catch(() => {});
|
||||||
@@ -1901,6 +1928,7 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||||||
extensionStatuses,
|
extensionStatuses,
|
||||||
extensionDialog,
|
extensionDialog,
|
||||||
toolsExpanded,
|
toolsExpanded,
|
||||||
|
showThinkingAndTools,
|
||||||
toggleSidebar: () => setSidebarOpen((o) => !o),
|
toggleSidebar: () => setSidebarOpen((o) => !o),
|
||||||
closeSidebar: () => setSidebarOpen(false),
|
closeSidebar: () => setSidebarOpen(false),
|
||||||
loadSessions,
|
loadSessions,
|
||||||
@@ -1916,6 +1944,7 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||||||
abortStream,
|
abortStream,
|
||||||
abortRetry,
|
abortRetry,
|
||||||
toggleToolsExpanded,
|
toggleToolsExpanded,
|
||||||
|
toggleShowThinkingAndTools,
|
||||||
respondExtensionDialog,
|
respondExtensionDialog,
|
||||||
dismissExtensionDialog,
|
dismissExtensionDialog,
|
||||||
applyModelSelection,
|
applyModelSelection,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { PromptFile } from "../api/prompts";
|
|||||||
import * as settingsApi from "../api/settings";
|
import * as settingsApi from "../api/settings";
|
||||||
import { useAvatars } from "../context/AvatarContext";
|
import { useAvatars } from "../context/AvatarContext";
|
||||||
import { useBootstrap } from "../context/BootstrapContext";
|
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 { AgentToolInfo, EnvironmentInfo, EnvironmentToolsInfo, ExtensionInfo, McpServerInfo, RuntimeInfo, SettingsPaneId, SkillInfo } from "../types/events";
|
||||||
import type { McpToolInfo } from "../types/events";
|
import type { McpToolInfo } from "../types/events";
|
||||||
import { renderMarkdown } from "../utils/markdown";
|
import { renderMarkdown } from "../utils/markdown";
|
||||||
@@ -309,6 +310,7 @@ function McpToolItem({
|
|||||||
export function SettingsPage() {
|
export function SettingsPage() {
|
||||||
const { markRouteReady } = useBootstrap();
|
const { markRouteReady } = useBootstrap();
|
||||||
const { updateAvatars } = useAvatars();
|
const { updateAvatars } = useAvatars();
|
||||||
|
const { showThinkingAndTools, toggleShowThinkingAndTools } = useChatContext();
|
||||||
const [activePane, setActivePane] = useState<SettingsPaneId>("prompt");
|
const [activePane, setActivePane] = useState<SettingsPaneId>("prompt");
|
||||||
const [systemPrompt, setSystemPrompt] = useState("");
|
const [systemPrompt, setSystemPrompt] = useState("");
|
||||||
const [systemPromptPath, setSystemPromptPath] = useState("");
|
const [systemPromptPath, setSystemPromptPath] = useState("");
|
||||||
@@ -1348,6 +1350,23 @@ export function SettingsPage() {
|
|||||||
<p className={styles.fieldHint}>
|
<p className={styles.fieldHint}>
|
||||||
留空则不显示对应头像。仅支持 http / https 图片链接。
|
留空则不显示对应头像。仅支持 http / https 图片链接。
|
||||||
</p>
|
</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>
|
</div>
|
||||||
{statusText && activePane === "other" ? (
|
{statusText && activePane === "other" ? (
|
||||||
<div className={`${styles.status} ${statusClass}`}>{statusText}</div>
|
<div className={`${styles.status} ${statusClass}`}>{statusText}</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user