From fa4144d67515557a67b91620600f840032fe0add Mon Sep 17 00:00:00 2001 From: ashishexee <144021866+ashishexee@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:01:21 +0530 Subject: [PATCH] feat(desktop): add expand_thinking config toggle (#3303) Also persists /verbose toggle across CLI sessions (#3312). Closes #3303 Closes #3312 --- desktop/frontend/src/App.tsx | 3 +++ desktop/frontend/src/components/Message.tsx | 4 +++- .../frontend/src/components/SettingsPanel.tsx | 14 ++++++++++++++ desktop/frontend/src/components/Transcript.tsx | 16 ++++++++++++---- desktop/frontend/src/lib/bridge.ts | 5 +++++ desktop/frontend/src/lib/types.ts | 1 + desktop/frontend/src/locales/en.ts | 3 +++ desktop/frontend/src/locales/zh.ts | 3 +++ desktop/settings_app.go | 9 +++++++++ internal/cli/chat_tui.go | 6 ++++++ internal/cli/cli.go | 2 ++ internal/cli/help_view.go | 1 + internal/config/config.go | 2 ++ internal/config/edit.go | 17 +++++++++++++++++ internal/config/render.go | 6 ++++++ 15 files changed, 87 insertions(+), 5 deletions(-) diff --git a/desktop/frontend/src/App.tsx b/desktop/frontend/src/App.tsx index 88eebe68a..08df612ac 100644 --- a/desktop/frontend/src/App.tsx +++ b/desktop/frontend/src/App.tsx @@ -418,6 +418,7 @@ export default function App() { const [projectRevision, setProjectRevision] = useState(0); const [composerInsertRequest, setComposerInsertRequest] = useState(null); const [desktopPlatform, setDesktopPlatform] = useState(detectBrowserPlatform); + const [expandThinking, setExpandThinking] = useState(false); const [renamingTopicId, setRenamingTopicId] = useState(null); const [topicTitleDraft, setTopicTitleDraft] = useState(""); const [topicExportOpen, setTopicExportOpen] = useState(false); @@ -465,6 +466,7 @@ export default function App() { const nextStyle = normalizeThemeStyleForTheme(settings.desktopThemeStyle, nextTheme); applyTheme(nextTheme, nextStyle, { persist: false }); setLocalePref(normalizeLangPref(settings.desktopLanguage)); + setExpandThinking(settings.expandThinking); }; void syncDesktopPreferences().catch((e) => { console.warn("desktop preferences sync failed", e); @@ -1612,6 +1614,7 @@ export default function App() { checkpoints={state.checkpoints} actionPending={state.messageAction != null} rewindDisabled={state.running || state.messageAction != null || state.approval != null || state.ask != null} + defaultExpandThinking={expandThinking} /> )} diff --git a/desktop/frontend/src/components/Message.tsx b/desktop/frontend/src/components/Message.tsx index 9c9c02e57..c490f1b9b 100644 --- a/desktop/frontend/src/components/Message.tsx +++ b/desktop/frontend/src/components/Message.tsx @@ -238,8 +238,10 @@ export function TurnActions({ export const AssistantMessage = memo(function AssistantMessage({ item, + defaultExpanded = false, }: { item: AssistantItem; + defaultExpanded?: boolean; }) { const t = useT(); const hasText = item.streaming || item.text.trim() !== ""; @@ -259,7 +261,7 @@ export const AssistantMessage = memo(function AssistantMessage({ {item.streaming ? t("msg.thinkingRunning") : t("msg.thinkingDone")} } - defaultOpen={item.streaming} + defaultOpen={item.streaming || defaultExpanded} >
{item.reasoning}
diff --git a/desktop/frontend/src/components/SettingsPanel.tsx b/desktop/frontend/src/components/SettingsPanel.tsx index 429759194..0c86b9b2c 100644 --- a/desktop/frontend/src/components/SettingsPanel.tsx +++ b/desktop/frontend/src/components/SettingsPanel.tsx @@ -515,6 +515,20 @@ function GeneralSection({ s, busy, apply }: SectionProps) { ))} + +
+ {([false, true] as const).map((val) => ( + + ))} +
+
{AUTO_PLAN_MODES.map((mode) => ( diff --git a/desktop/frontend/src/components/Transcript.tsx b/desktop/frontend/src/components/Transcript.tsx index 92b3a2511..a0b9cf05f 100644 --- a/desktop/frontend/src/components/Transcript.tsx +++ b/desktop/frontend/src/components/Transcript.tsx @@ -17,10 +17,10 @@ type QuestionAnchor = { id: string; text: string; turn: number }; const QUESTION_NAV_MIN_COUNT = 2; const LiveStreamContext = createContext(undefined); -const LiveAssistantMessage = memo(function LiveAssistantMessage({ item }: { item: AssistantItem }) { +const LiveAssistantMessage = memo(function LiveAssistantMessage({ item, defaultExpanded = false }: { item: AssistantItem; defaultExpanded?: boolean }) { const live = useContext(LiveStreamContext); const shown = live && live.id === item.id ? { ...item, text: live.text, reasoning: live.reasoning, streaming: true } : item; - return ; + return ; }); // ── Layer budgets ───────────────────────────────────────────────────────────── @@ -151,6 +151,7 @@ export function Transcript({ actionPending = false, rewindDisabled = false, questionNavigator = true, + defaultExpandThinking = false, }: { items: Item[]; live?: LiveStream; @@ -161,6 +162,7 @@ export function Transcript({ actionPending?: boolean; rewindDisabled?: boolean; questionNavigator?: boolean; + defaultExpandThinking?: boolean; }) { const scrollRef = useRef(null); const stick = useRef(true); @@ -377,7 +379,7 @@ export function Transcript({ break; } case "assistant": - out.push(); + out.push(); if (!it.streaming && it.text.trim() !== "") { actionText = it.text; actionReady = true; @@ -431,6 +433,7 @@ export function Transcript({ warmRewindDisabled={rewindDisabled} warmOnRewind={onRewind} warmSetOpenAction={setOpenAction} + defaultExpandThinking={defaultExpandThinking} onToggleColdPage={() => setColdPage((p) => p + 1)} onToggleWarmTurn={(g, expand) => { setExpandedWarmTurns((prev) => { @@ -466,6 +469,7 @@ const WarmZone = memo(function WarmZone({ warmRewindDisabled, warmOnRewind, warmSetOpenAction, + defaultExpandThinking = false, onToggleColdPage, onToggleWarmTurn, }: { @@ -483,6 +487,7 @@ const WarmZone = memo(function WarmZone({ warmRewindDisabled: boolean; warmOnRewind: ((turn: number, scope: string) => void) | undefined; warmSetOpenAction: (action: OpenTurnAction | null) => void; + defaultExpandThinking?: boolean; onToggleColdPage: () => void; onToggleWarmTurn: (g: number, expand: boolean) => void; }) { @@ -537,6 +542,7 @@ const WarmZone = memo(function WarmZone({ rewindDisabled={warmRewindDisabled} onRewind={warmOnRewind} setOpenAction={warmSetOpenAction} + defaultExpandThinking={defaultExpandThinking} /> , ); @@ -580,6 +586,7 @@ function WarmTurnItems({ rewindDisabled, onRewind, setOpenAction, + defaultExpandThinking = false, }: { startIdx: number; endIdx: number; @@ -592,6 +599,7 @@ function WarmTurnItems({ rewindDisabled: boolean; onRewind: ((turn: number, scope: string) => void) | undefined; setOpenAction: (action: OpenTurnAction | null) => void; + defaultExpandThinking?: boolean; }) { const nodes: React.ReactNode[] = []; let actionText = ""; @@ -634,7 +642,7 @@ function WarmTurnItems({ break; } case "assistant": { - nodes.push(); + nodes.push(); if (!it.streaming && it.text.trim() !== "") { actionText = it.text; actionReady = true; diff --git a/desktop/frontend/src/lib/bridge.ts b/desktop/frontend/src/lib/bridge.ts index aac910a93..9bc3b0dfa 100644 --- a/desktop/frontend/src/lib/bridge.ts +++ b/desktop/frontend/src/lib/bridge.ts @@ -188,6 +188,7 @@ export interface AppBindings { SetCloseBehavior(mode: string): Promise; SetDesktopLanguage(lang: string): Promise; SetDesktopAppearance(theme: string, style: string): Promise; + SetExpandThinking(on: boolean): Promise; MigrateDesktopPreferences(language: string, theme: string, style: string): Promise; SetAgentParams(temperature: number, maxSteps: number, plannerMaxSteps: number, systemPrompt: string): Promise; SetTrayLocale(locale: "en" | "zh"): Promise; @@ -574,6 +575,7 @@ function makeMockApp(): AppBindings { desktopTheme: "dark", desktopThemeStyle: "graphite", closeBehavior: "background", + expandThinking: false, configPath: "~/projects/reasonix/reasonix.toml", providerKinds: ["openai"], bypass: false, @@ -1543,6 +1545,9 @@ function makeMockApp(): AppBindings { settings.desktopTheme = theme === "auto" || theme === "light" ? theme : "dark"; settings.desktopThemeStyle = style; }, + async SetExpandThinking(on: boolean) { + settings.expandThinking = on; + }, async MigrateDesktopPreferences(language: string, theme: string, style: string) { if (!settings.desktopLanguage) settings.desktopLanguage = language === "en" || language === "zh" ? language : ""; if (!settings.desktopTheme && !settings.desktopThemeStyle) { diff --git a/desktop/frontend/src/lib/types.ts b/desktop/frontend/src/lib/types.ts index c516756d5..ae512454d 100644 --- a/desktop/frontend/src/lib/types.ts +++ b/desktop/frontend/src/lib/types.ts @@ -553,6 +553,7 @@ export interface SettingsView { desktopTheme: string; // "auto" | "dark" | "light" desktopThemeStyle: string; closeBehavior: string; // "background" | "quit" + expandThinking: boolean; // show reasoning text expanded by default configPath: string; providerKinds: string[]; // provider implementations the kernel registered (for the kind picker) bypass: boolean; // live YOLO state (runtime-only) — whether approvals are skipped this session diff --git a/desktop/frontend/src/locales/en.ts b/desktop/frontend/src/locales/en.ts index 53fd08c53..27cae9b3e 100644 --- a/desktop/frontend/src/locales/en.ts +++ b/desktop/frontend/src/locales/en.ts @@ -474,6 +474,9 @@ export const en = { "settings.closeBehavior": "When closing window", "settings.closeBehavior.background": "Keep running", "settings.closeBehavior.quit": "Quit Reasonix", + "settings.expandThinking": "Thinking display", + "settings.expandThinking.expanded": "Expanded", + "settings.expandThinking.collapsed": "Collapsed", "settings.manageProviders": "Manage providers", "settings.activeProvider": "Active provider", "settings.plannerStatus": "Planning mode", diff --git a/desktop/frontend/src/locales/zh.ts b/desktop/frontend/src/locales/zh.ts index 080f3e502..0c301cec2 100644 --- a/desktop/frontend/src/locales/zh.ts +++ b/desktop/frontend/src/locales/zh.ts @@ -476,6 +476,9 @@ export const zh: Record = { "settings.closeBehavior": "关闭窗口时", "settings.closeBehavior.background": "保持后台运行", "settings.closeBehavior.quit": "退出 Reasonix", + "settings.expandThinking": "思考过程显示", + "settings.expandThinking.expanded": "默认展开", + "settings.expandThinking.collapsed": "默认折叠", "settings.manageProviders": "管理模型服务", "settings.activeProvider": "当前模型服务", "settings.plannerStatus": "规划方式", diff --git a/desktop/settings_app.go b/desktop/settings_app.go index 36dd06cfd..69e0961a7 100644 --- a/desktop/settings_app.go +++ b/desktop/settings_app.go @@ -96,6 +96,7 @@ type SettingsView struct { DesktopTheme string `json:"desktopTheme"` DesktopThemeStyle string `json:"desktopThemeStyle"` CloseBehavior string `json:"closeBehavior"` + ExpandThinking bool `json:"expandThinking"` ConfigPath string `json:"configPath"` // ProviderKinds lists the provider implementations the kernel actually // registered (provider.Kinds()), so the editor's "kind" picker offers only @@ -268,6 +269,7 @@ func (a *App) Settings() SettingsView { DesktopTheme: "dark", DesktopThemeStyle: "graphite", CloseBehavior: "background", + ExpandThinking: false, } } ctrl := a.activeCtrl() @@ -310,6 +312,7 @@ func (a *App) Settings() SettingsView { DesktopTheme: cfg.DesktopTheme(), DesktopThemeStyle: cfg.DesktopThemeStyle(), CloseBehavior: cfg.DesktopCloseBehavior(), + ExpandThinking: cfg.Desktop.ExpandThinking, ConfigPath: cfgPath, ProviderKinds: nonNil(provider.Kinds()), Bypass: ctrl != nil && ctrl.Bypass(), @@ -1085,6 +1088,12 @@ func (a *App) SetDesktopAppearance(theme, style string) error { return a.applyConfigOnly(func(c *config.Config) error { return c.SetDesktopAppearance(theme, style) }) } +// SetExpandThinking sets whether reasoning text is expanded by default on +// the desktop. It is desktop-only and does not rebuild the controller. +func (a *App) SetExpandThinking(on bool) error { + return a.applyConfigOnly(func(c *config.Config) error { return c.SetExpandThinking(on) }) +} + // MigrateDesktopPreferences imports old browser-local desktop preferences into // the user config once. Existing [desktop] values win so stale localStorage never // overwrites an explicit config edit. diff --git a/internal/cli/chat_tui.go b/internal/cli/chat_tui.go index c0dcb7f50..d6194d393 100644 --- a/internal/cli/chat_tui.go +++ b/internal/cli/chat_tui.go @@ -24,6 +24,7 @@ import ( "reasonix/internal/agent" "reasonix/internal/command" + "reasonix/internal/config" "reasonix/internal/control" "reasonix/internal/event" "reasonix/internal/hook" @@ -117,6 +118,7 @@ type chatTUI struct { pendingCommit *[]string renderer *mdRenderer showReasoning bool // Ctrl+O / /verbose: show raw thinking text in the CLI + cfg *config.Config // reasoningLineIdx is the transcript index of the live "▎ thinking…" marker // while a reasoning block streams; it's rewritten to "▎ thought for Ns" when // the block closes. -1 when no block is open. transcriptDirty forces a @@ -2681,6 +2683,10 @@ func (m *chatTUI) cycleMode() { func (m *chatTUI) toggleVerboseReasoning(notify bool) { m.showReasoning = !m.showReasoning + if m.cfg != nil { + _ = m.cfg.SetShowReasoning(m.showReasoning) + _ = m.cfg.Save() + } if !notify { return } diff --git a/internal/cli/cli.go b/internal/cli/cli.go index df03bf719..1ce692b20 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -435,6 +435,8 @@ func chatREPL(args []string) int { if cfg, err := config.Load(); err == nil { m.outputStyle = cfg.Agent.OutputStyle // shown as the active entry in /output-style m.statuslineCmd = cfg.Statusline.Command // custom status-line command, "" = built-in row + m.showReasoning = cfg.UI.ShowReasoning // /verbose persistence: start with config default + m.cfg = cfg } // /model support: a pure builder the TUI calls to rebuild on a different diff --git a/internal/cli/help_view.go b/internal/cli/help_view.go index 8dfa8ce32..8e60efe95 100644 --- a/internal/cli/help_view.go +++ b/internal/cli/help_view.go @@ -69,6 +69,7 @@ func builtinHelpItems() []compItem { {label: "/hooks", hint: i18n.M.CmdHooks}, {label: "/memory", hint: i18n.M.CmdMemory}, {label: "/output-style", hint: i18n.M.CmdOutputStyle}, + {label: "/verbose", hint: i18n.M.CmdVerbose}, {label: "/language", hint: i18n.M.CmdLanguage}, {label: "/auto-plan", hint: i18n.M.CmdAutoPlan}, {label: "/help", hint: i18n.M.CmdHelp}, diff --git a/internal/config/config.go b/internal/config/config.go index e4741a5de..822cf349a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -64,6 +64,7 @@ type UIConfig struct { Theme string `toml:"theme"` // auto|dark|light; empty resolves to auto ThemeStyle string `toml:"theme_style"` // graphite|ember|aurora|midnight|sandstone|porcelain|linen|glacier CloseBehavior string `toml:"close_behavior"` // legacy desktop close behavior; prefer desktop.close_behavior + ShowReasoning bool `toml:"show_reasoning"` // Ctrl+O / /verbose: show thinking text in CLI; false = collapsed } // DesktopConfig controls desktop-only UI preferences. It is intentionally @@ -75,6 +76,7 @@ type DesktopConfig struct { ThemeStyle string `toml:"theme_style"` // graphite|ember|aurora|midnight|sandstone|porcelain|linen|glacier CloseBehavior string `toml:"close_behavior"` // quit|background; desktop window close behavior 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 } // NotificationsConfig controls optional system notifications for CLI chat/run. diff --git a/internal/config/edit.go b/internal/config/edit.go index a4742ed38..136751ef5 100644 --- a/internal/config/edit.go +++ b/internal/config/edit.go @@ -175,6 +175,23 @@ func (c *Config) SetUICloseBehavior(mode string) error { return c.SetDesktopCloseBehavior(mode) } +// SetExpandThinking sets whether the desktop reasoning/thinking section is +// expanded by default. It is desktop-only and must not affect CLI output or +// provider-visible request data. +func (c *Config) SetExpandThinking(on bool) error { + c.Desktop.ExpandThinking = on + return nil +} + +// SetShowReasoning sets the CLI's default verbose-reasoning preference. When +// true, thinking text is shown in the chat TUI on startup; when false (the +// default), it stays collapsed until the user toggles it with Ctrl+O or +// /verbose. +func (c *Config) SetShowReasoning(on bool) error { + c.UI.ShowReasoning = on + 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/render.go b/internal/config/render.go index f4996d751..ab1db7a48 100644 --- a/internal/config/render.go +++ b/internal/config/render.go @@ -63,6 +63,11 @@ func RenderTOMLForScope(c *Config, scope RenderScope) string { if strings.TrimSpace(c.UI.CloseBehavior) != "" && scope == RenderScopeProject { fmt.Fprintf(&b, "close_behavior = %q # legacy desktop close behavior; prefer [desktop].close_behavior in user config\n", c.DesktopCloseBehavior()) } + if c.UI.ShowReasoning { + b.WriteString("show_reasoning = true # CLI: show thinking text by default; false = collapsed (toggle with Ctrl+O)\n") + } else { + b.WriteString("# show_reasoning = true # CLI: show thinking text by default; false = collapsed (toggle with Ctrl+O)\n") + } b.WriteString("\n") } @@ -83,6 +88,7 @@ func RenderTOMLForScope(c *Config, scope RenderScope) string { 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)) } + fmt.Fprintf(&b, "expand_thinking = %v # desktop: show reasoning text expanded by default; false = collapsed\n", c.Desktop.ExpandThinking) b.WriteString("\n") b.WriteString("[notifications]\n")