From 905da5e8051ee878a22d6a4ada130f99ea1060ce Mon Sep 17 00:00:00 2001 From: SuMuxi66 <2728602302@qq.com> Date: Wed, 10 Jun 2026 12:22:55 +0800 Subject: [PATCH] feat(desktop): add toggle for tool call visibility Add a global setting to control whether tool call blocks are shown in the chat area. The toggle defaults to on (show), persists to the desktop TOML config, and only affects UI rendering. Functional tool UI (todo_write, exit_plan_mode) remains visible regardless of the setting. Closes: esengine/DeepSeek-Reasonix#3710 --- desktop/frontend/src/App.tsx | 8 +++ .../frontend/src/components/SettingsPanel.tsx | 43 ++++++++++++++-- .../frontend/src/components/Transcript.tsx | 14 ++++- desktop/frontend/src/lib/bridge.ts | 9 ++++ desktop/frontend/src/lib/types.ts | 1 + desktop/frontend/src/locales/en.ts | 4 ++ desktop/frontend/src/locales/zh.ts | 4 ++ desktop/settings_app.go | 10 ++++ internal/config/config.go | 11 ++++ internal/config/edit.go | 9 ++++ internal/config/edit_test.go | 51 +++++++++++++++++++ internal/config/render.go | 5 ++ 12 files changed, 164 insertions(+), 5 deletions(-) diff --git a/desktop/frontend/src/App.tsx b/desktop/frontend/src/App.tsx index f24fb8154..a4e8421eb 100644 --- a/desktop/frontend/src/App.tsx +++ b/desktop/frontend/src/App.tsx @@ -872,6 +872,10 @@ export default function App() { hydrateDisplayMode(settings.displayMode); setSidebarImConnections(sidebarImConnectionsFromBot(settings.bot, t)); setImTopicSources(sidebarImTopicSourcesFromBot(settings.bot, t)); + if (!showToolCallsLoaded.current) { + showToolCallsLoaded.current = true; + setShowToolCalls(settings.showToolCalls !== false); + } }; void syncDesktopPreferences().catch((e) => { console.warn("desktop preferences sync failed", e); @@ -914,6 +918,8 @@ export default function App() { const [pendingPlanRevision, setPendingPlanRevision] = useState(null); const [footerHeight, setFooterHeight] = useState(0); const footerHeightRef = useRef(0); + const [showToolCalls, setShowToolCalls] = useState(true); + const showToolCallsLoaded = useRef(false); const footerRef = useRef(null); const runningRef = useRef(state.running); const rightDockDetailActive = rightDockMode !== "context" && workspacePreviewActive; @@ -2557,6 +2563,7 @@ export default function App() { actionPending={state.messageAction != null} rewindDisabled={state.running || state.messageAction != null || state.approval != null || state.ask != null || clearContextPending} defaultExpandThinking={expandThinking} + showToolCalls={showToolCalls} /> )} @@ -2779,6 +2786,7 @@ export default function App() { .then(applyDesktopPreferences) .catch((e) => console.warn("desktop preferences refresh failed", e)); }} + onShowToolCallsChange={setShowToolCalls} /> )} diff --git a/desktop/frontend/src/components/SettingsPanel.tsx b/desktop/frontend/src/components/SettingsPanel.tsx index c525773b8..1167b77df 100644 --- a/desktop/frontend/src/components/SettingsPanel.tsx +++ b/desktop/frontend/src/components/SettingsPanel.tsx @@ -33,7 +33,7 @@ const SETTINGS_TABS: SettingsTab[] = ["general", "models", "bots", "mcp", "skill // SettingsPanel is the desktop settings centre — a centred modal with left // navigation and a right content area. It hosts all settings pages plus MCP, // Skills, and Memory management, replacing the old per-feature drawers. -export function SettingsPanel({ onClose, onChanged, initialTab, isDevBuild }: { onClose: () => void; onChanged: () => void; initialTab?: SettingsTab; isDevBuild?: boolean }) { +export function SettingsPanel({ onClose, onChanged, initialTab, isDevBuild, onShowToolCallsChange }: { onClose: () => void; onChanged: () => void; initialTab?: SettingsTab; isDevBuild?: boolean; onShowToolCallsChange?: (show: boolean) => void }) { const t = useT(); const [s, setS] = useState(null); const [busy, setBusy] = useState(false); @@ -59,6 +59,18 @@ export function SettingsPanel({ onClose, onChanged, initialTab, isDevBuild }: { setThemeStyleState(nextStyle); }, [s?.desktopTheme, s?.desktopThemeStyle]); + // Push the persisted showToolCalls flag into App-level state once on load, so + // the transcript honours the persisted preference even if the user never opens + // Settings. We do not subscribe to s?.showToolCalls here — that would feed our + // own optimistic updates back into App and cause stale-button flicker. + const syncedShowToolCalls = useRef(false); + useEffect(() => { + if (s && !syncedShowToolCalls.current) { + syncedShowToolCalls.current = true; + onShowToolCallsChange?.(s.showToolCalls !== false); + } + }, [s, onShowToolCallsChange]); + // apply runs a mutation, re-reads settings, and refreshes the topbar/model. const apply = async (fn: () => Promise) => { setBusy(true); @@ -125,7 +137,7 @@ export function SettingsPanel({ onClose, onChanged, initialTab, isDevBuild }: {
{t("settings.loading")}
) : ( <> - {tab === "general" && s && } + {tab === "general" && s && setS((prev) => (prev ? { ...prev, showToolCalls: next } : prev))} />} {tab === "models" && s && } {tab === "bots" && isDevBuild && s && } {tab === "mcp" && } @@ -636,7 +648,7 @@ function reasoningProtocolLabel(protocol: string, t: ReturnType): s } } -function GeneralSection({ s, busy, apply }: SectionProps) { +function GeneralSection({ s, busy, apply, onShowToolCallsChange, onOptimisticShowToolCalls }: SectionProps & { onShowToolCallsChange?: (show: boolean) => void; onOptimisticShowToolCalls?: (show: boolean) => void }) { const { t, setPref } = useI18n(); const closeBehavior = normalizeCloseBehavior(s.closeBehavior); const [displayMode, setDisplayMode] = useState(() => normalizeDisplayMode(getDisplayMode())); @@ -647,6 +659,13 @@ function GeneralSection({ s, busy, apply }: SectionProps) { setPref(next); void apply(() => app.SetDesktopLanguage(next)); }; + const showToolCalls = s.showToolCalls !== false; + const setShowToolCalls = (next: boolean) => { + if (next === showToolCalls) return; + onOptimisticShowToolCalls?.(next); + onShowToolCallsChange?.(next); + void apply(() => app.SetShowToolCalls(next)); + }; return ( @@ -722,6 +741,24 @@ function GeneralSection({ s, busy, apply }: SectionProps) { ))} + +
+ + +
+
); } diff --git a/desktop/frontend/src/components/Transcript.tsx b/desktop/frontend/src/components/Transcript.tsx index 6efb6b191..ac85027a1 100644 --- a/desktop/frontend/src/components/Transcript.tsx +++ b/desktop/frontend/src/components/Transcript.tsx @@ -169,6 +169,7 @@ export function Transcript({ rewindDisabled = false, questionNavigator = true, defaultExpandThinking = false, + showToolCalls = true, }: { items: Item[]; live?: LiveStream; @@ -180,6 +181,7 @@ export function Transcript({ rewindDisabled?: boolean; questionNavigator?: boolean; defaultExpandThinking?: boolean; + showToolCalls?: boolean; }) { const scrollRef = useRef(null); const stick = useRef(true); @@ -435,6 +437,7 @@ export function Transcript({ if (it.parentId) break; if (it.name === "todo_write") break; if (it.name === "exit_plan_mode") break; + if (!showToolCalls) break; out.push(); break; case "phase": out.push(); break; @@ -489,6 +492,7 @@ export function Transcript({ return next; }); }} + showToolCalls={showToolCalls} /> )} {hotZoneNodes} @@ -519,6 +523,7 @@ const WarmZone = memo(function WarmZone({ defaultExpandThinking = false, onToggleColdPage, onToggleWarmTurn, + showToolCalls = true, }: { turnGroups: TurnGroup[]; expandedWarmTurns: ReadonlySet; @@ -532,11 +537,12 @@ const WarmZone = memo(function WarmZone({ warmOpenAction: OpenTurnAction | null; warmActionPending: boolean; warmRewindDisabled: boolean; - warmOnRewind: ((turn: number, scope: string) => void) | undefined; + warmOnRewind?: (turn: number, scope: string) => void; warmSetOpenAction: (action: OpenTurnAction | null) => void; defaultExpandThinking?: boolean; onToggleColdPage: () => void; onToggleWarmTurn: (g: number, expand: boolean) => void; + showToolCalls?: boolean; }) { const t = useT(); const out: React.ReactNode[] = []; @@ -590,6 +596,7 @@ const WarmZone = memo(function WarmZone({ onRewind={warmOnRewind} setOpenAction={warmSetOpenAction} defaultExpandThinking={defaultExpandThinking} + showToolCalls={showToolCalls} /> , ); @@ -634,6 +641,7 @@ function WarmTurnItems({ onRewind, setOpenAction, defaultExpandThinking = false, + showToolCalls = true, }: { startIdx: number; endIdx: number; @@ -644,9 +652,10 @@ function WarmTurnItems({ openAction: OpenTurnAction | null; actionPending: boolean; rewindDisabled: boolean; - onRewind: ((turn: number, scope: string) => void) | undefined; + onRewind?: (turn: number, scope: string) => void; setOpenAction: (action: OpenTurnAction | null) => void; defaultExpandThinking?: boolean; + showToolCalls?: boolean; }) { const nodes: React.ReactNode[] = []; let actionText = ""; @@ -716,6 +725,7 @@ function WarmTurnItems({ if (it.parentId) break; if (it.name === "todo_write") break; if (it.name === "exit_plan_mode") break; + if (!showToolCalls) break; nodes.push(); break; } diff --git a/desktop/frontend/src/lib/bridge.ts b/desktop/frontend/src/lib/bridge.ts index 3a33a77fd..df98ae362 100644 --- a/desktop/frontend/src/lib/bridge.ts +++ b/desktop/frontend/src/lib/bridge.ts @@ -92,6 +92,7 @@ export interface AppBindings { SubmitToTab(tabID: string, input: string): Promise; SubmitDisplay(display: string, input: string): Promise; SubmitDisplayToTab(tabID: string, display: string, input: string): Promise; + SubmitDisplayToTabWithRefs(tabID: string, display: string, input: string, refs: string): Promise; RunShell(command: string): Promise; RunShellForTab(tabID: string, command: string): Promise; Steer(text: string): Promise; @@ -224,6 +225,7 @@ export interface AppBindings { TestBotConnection(id: string, target?: string): Promise; SetCloseBehavior(mode: string): Promise; SetDisplayMode(mode: string): Promise; + SetShowToolCalls(show: boolean): Promise; SetDesktopLanguage(lang: string): Promise; SetDesktopAppearance(theme: string, style: string): Promise; SetDesktopCheckUpdates(enabled: boolean): Promise; @@ -785,6 +787,7 @@ function makeMockApp(): AppBindings { telemetry: true, metrics: false, expandThinking: false, + showToolCalls: true, configPath: "~/projects/reasonix/reasonix.toml", providerKinds: ["openai"], autoApproveTools: false, @@ -1375,6 +1378,9 @@ function makeMockApp(): AppBindings { async SubmitDisplayToTab(_tabID, display, input) { await withMockTabScope(_tabID, () => this.SubmitDisplay(display, input)); }, + async SubmitDisplayToTabWithRefs(_tabID, display, input, _refs) { + await withMockTabScope(_tabID, () => this.SubmitDisplay(display, input)); + }, async RunShell(command) { cancelled = false; emitMockTurnStarted(); @@ -2249,6 +2255,9 @@ function makeMockApp(): AppBindings { async SetDisplayMode(mode: string) { settings.displayMode = mode; }, + async SetShowToolCalls(show: boolean) { + settings.showToolCalls = Boolean(show); + }, async SetDesktopLanguage(lang: string) { settings.desktopLanguage = lang === "en" || lang === "zh" ? lang : ""; }, diff --git a/desktop/frontend/src/lib/types.ts b/desktop/frontend/src/lib/types.ts index a6312e66f..4eaf8d490 100644 --- a/desktop/frontend/src/lib/types.ts +++ b/desktop/frontend/src/lib/types.ts @@ -753,6 +753,7 @@ export interface SettingsView { telemetry: boolean; // anonymous launch ping (install id + version + OS) metrics: boolean; // opt-in aggregate agent metrics (anonymous signal/bucket counts) expandThinking: boolean; // show reasoning text expanded by default + showToolCalls: boolean; // whether the transcript renders ToolCard blocks configPath: string; providerKinds: string[]; // provider implementations the kernel registered (for the kind picker) autoApproveTools: boolean; diff --git a/desktop/frontend/src/locales/en.ts b/desktop/frontend/src/locales/en.ts index ade6843f0..729c8bcb8 100644 --- a/desktop/frontend/src/locales/en.ts +++ b/desktop/frontend/src/locales/en.ts @@ -17,6 +17,8 @@ export const en = { "common.collapse": "Collapse", "common.none": "none", "common.auto": "auto", + "common.on": "On", + "common.off": "Off", "common.busyHint": "Finish or stop the current turn first", "common.loading": "Loading…", "app.splashSubtitle": "Agent Workspace", @@ -661,6 +663,8 @@ export const en = { "settings.autoPlan": "Automatic plan mode", "settings.autoPlan.off": "Off", "settings.autoPlan.on": "On", + "settings.showToolCalls": "Show tool calls", + "settings.showToolCallsHint": "Render tool call and tool result blocks in the transcript. Off hides them; agent execution and history are unaffected.", "settings.agentRuntime": "Agent runtime", "settings.agentRuntimeHint": "Saved as user defaults. A project's ./reasonix.toml can override them; use 0 for no limit.", "settings.executorMaxSteps": "Executor step limit", diff --git a/desktop/frontend/src/locales/zh.ts b/desktop/frontend/src/locales/zh.ts index 12df8afaf..b51bb42ed 100644 --- a/desktop/frontend/src/locales/zh.ts +++ b/desktop/frontend/src/locales/zh.ts @@ -18,6 +18,8 @@ export const zh: Record = { "common.collapse": "收起", "common.none": "无", "common.auto": "自动", + "common.on": "开启", + "common.off": "关闭", "common.busyHint": "请先完成或停止当前回合", "common.loading": "加载中…", "app.splashSubtitle": "Agent Workspace", @@ -663,6 +665,8 @@ export const zh: Record = { "settings.autoPlan": "自动计划模式", "settings.autoPlan.off": "关闭", "settings.autoPlan.on": "开启", + "settings.showToolCalls": "显示工具调用", + "settings.showToolCallsHint": "在对话中渲染工具调用与结果块。关闭后仅隐藏显示,不影响 agent 执行与历史。", "settings.agentRuntime": "Agent 运行", "settings.agentRuntimeHint": "保存为用户级默认值。项目的 ./reasonix.toml 可以覆盖;设为 0 表示不限。", "settings.executorMaxSteps": "执行轮数上限", diff --git a/desktop/settings_app.go b/desktop/settings_app.go index a778ce48b..cc2153e8d 100644 --- a/desktop/settings_app.go +++ b/desktop/settings_app.go @@ -152,6 +152,7 @@ type SettingsView struct { Telemetry bool `json:"telemetry"` Metrics bool `json:"metrics"` ExpandThinking bool `json:"expandThinking"` + ShowToolCalls bool `json:"showToolCalls"` ConfigPath string `json:"configPath"` // ProviderKinds lists the provider implementations the kernel actually // registered (provider.Kinds()), so the editor's "kind" picker offers only @@ -333,6 +334,7 @@ func (a *App) Settings() SettingsView { Telemetry: true, Metrics: false, ExpandThinking: false, + ShowToolCalls: true, } } ctrl := a.activeCtrl() @@ -381,6 +383,7 @@ func (a *App) Settings() SettingsView { Telemetry: cfg.DesktopTelemetry(), Metrics: cfg.DesktopMetrics(), ExpandThinking: cfg.Desktop.ExpandThinking, + ShowToolCalls: cfg.DesktopShowToolCalls(), ConfigPath: cfgPath, ProviderKinds: nonNil(provider.Kinds()), AutoApproveTools: ctrl != nil && ctrl.AutoApproveTools(), @@ -1283,6 +1286,13 @@ func (a *App) SetDisplayMode(mode string) error { return a.applyConfigOnly(func(c *config.Config) error { return c.SetDesktopDisplayMode(mode) }) } +// SetShowToolCalls toggles whether the desktop transcript renders ToolCard +// blocks. It is a UI-only preference and does not affect agent execution, +// tool invocation, message history, or provider-visible request data. +func (a *App) SetShowToolCalls(show bool) error { + return a.applyConfigOnly(func(c *config.Config) error { return c.SetDesktopShowToolCalls(show) }) +} + // SetDesktopLanguage updates only the desktop UI language. It deliberately does // not touch config.language, which the CLI/model-facing runtime uses. func (a *App) SetDesktopLanguage(lang string) error { diff --git a/internal/config/config.go b/internal/config/config.go index 5e45f7a75..52131822b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -82,6 +82,7 @@ type DesktopConfig struct { CheckUpdates *bool `toml:"check_updates"` // startup update checks; nil keeps the default enabled Telemetry *bool `toml:"telemetry"` // anonymous launch ping (install id + version + OS); nil keeps the default enabled Metrics *bool `toml:"metrics"` // opt-in aggregate agent metrics (anonymous signal/bucket counts; no content); nil = disabled + ShowToolCalls *bool `toml:"show_tool_calls"` // nil/true renders tool calls; false hides them. UI-only. ProviderAccess []string `toml:"provider_access"` // desktop-only list of provider entries shown in Settings > Model > Access ExpandThinking bool `toml:"expand_thinking"` // true = show reasoning text expanded by default; false = collapsed } @@ -234,6 +235,16 @@ func (c *Config) DesktopMetrics() bool { return *c.Desktop.Metrics } +// DesktopShowToolCalls reports whether the desktop transcript should render +// ToolCard blocks. Defaults to true; an explicit false hides them. UI-only — +// does not affect agent execution, tool invocation, or persisted messages. +func (c *Config) DesktopShowToolCalls() bool { + if c.Desktop.ShowToolCalls == nil { + return true + } + return *c.Desktop.ShowToolCalls +} + // LSPConfig governs the optional Language Server Protocol tools (lsp_definition, // lsp_references, lsp_hover, lsp_diagnostics). Enabled defaults to true; the // servers themselves are never bundled — each resolves on PATH and the tool diff --git a/internal/config/edit.go b/internal/config/edit.go index c6b21d5ab..c88ad98a0 100644 --- a/internal/config/edit.go +++ b/internal/config/edit.go @@ -248,6 +248,15 @@ func (c *Config) SetShowReasoning(on bool) error { return nil } +// SetDesktopShowToolCalls toggles the UI-only desktop transcript tool-call +// visibility. It is intentionally a desktop render concern and must not +// affect agent execution, tool invocation, or persisted messages. +func (c *Config) SetDesktopShowToolCalls(show bool) error { + v := show + c.Desktop.ShowToolCalls = &v + return nil +} + // SetProviderThinking updates a provider's provider-specific thinking mode knob. func (c *Config) SetProviderThinking(name, thinking string) error { for i := range c.Providers { diff --git a/internal/config/edit_test.go b/internal/config/edit_test.go index 75852d378..60cd6ea27 100644 --- a/internal/config/edit_test.go +++ b/internal/config/edit_test.go @@ -816,6 +816,57 @@ func TestEffortCapabilityCustomSupportedEfforts(t *testing.T) { } } +// TestDesktopShowToolCallsRoundTrip locks in the round-trip for the desktop-only +// show_tool_calls preference. A nil pointer must default to true; explicit +// true and false must survive save → re-read. +func TestDesktopShowToolCallsRoundTrip(t *testing.T) { + t.Run("nil defaults to true", func(t *testing.T) { + c := Default() + c.Desktop.ShowToolCalls = nil + if got := c.DesktopShowToolCalls(); got != true { + t.Fatalf("nil Desktop.ShowToolCalls should default to true, got %v", got) + } + }) + t.Run("explicit false round-trips", func(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + path := UserConfigPath() + c := Default() + if err := c.SetDesktopShowToolCalls(false); err != nil { + t.Fatalf("SetDesktopShowToolCalls(false): %v", err) + } + if err := c.SaveTo(path); err != nil { + t.Fatalf("SaveTo: %v", err) + } + body, err := os.ReadFile(path) + if err != nil { + t.Fatalf("ReadFile: %v", err) + } + if !strings.Contains(string(body), "show_tool_calls = false") { + t.Fatalf("expected `show_tool_calls = false` in saved toml, got:\n%s", string(body)) + } + // Re-read from disk and assert the boolean survives. + reloaded := LoadForEdit(path) + if got := reloaded.DesktopShowToolCalls(); got != false { + t.Fatalf("reloaded show_tool_calls = %v, want false", got) + } + }) + t.Run("explicit true round-trips", func(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + path := UserConfigPath() + c := Default() + if err := c.SetDesktopShowToolCalls(true); err != nil { + t.Fatalf("SetDesktopShowToolCalls(true): %v", err) + } + if err := c.SaveTo(path); err != nil { + t.Fatalf("SaveTo: %v", err) + } + reloaded := LoadForEdit(path) + if got := reloaded.DesktopShowToolCalls(); got != true { + t.Fatalf("reloaded show_tool_calls = %v, want true", got) + } + }) +} + func TestEffortCapabilityUsesKnownModelRegistry(t *testing.T) { e := &ProviderEntry{ Name: "deepseek-proxy", diff --git a/internal/config/render.go b/internal/config/render.go index c9eca896e..1f481b6d2 100644 --- a/internal/config/render.go +++ b/internal/config/render.go @@ -93,6 +93,11 @@ func RenderTOMLForScope(c *Config, scope RenderScope) string { fmt.Fprintf(&b, "check_updates = %v # desktop: check for new versions on startup\n", c.DesktopCheckUpdates()) fmt.Fprintf(&b, "telemetry = %v # desktop: anonymous launch ping (install id + version + OS); never content\n", c.DesktopTelemetry()) fmt.Fprintf(&b, "metrics = %v # desktop: opt-in aggregate agent metrics (anonymous signal/bucket counts); never content\n", c.DesktopMetrics()) + if c.Desktop.ShowToolCalls != nil { + fmt.Fprintf(&b, "show_tool_calls = %v # desktop: render ToolCard blocks in the transcript; off hides them (UI only)\n", *c.Desktop.ShowToolCalls) + } else { + b.WriteString("# show_tool_calls = true # desktop: render ToolCard blocks in the transcript; off hides them (UI only)\n") + } if len(c.Desktop.ProviderAccess) > 0 { fmt.Fprintf(&b, "provider_access = %s # desktop settings: providers shown on Settings > Model > Access\n", renderStringArray(c.Desktop.ProviderAccess)) }