Default desktop reasoning to live follow

Problem:
Fresh installs still selected summary mode, so the new settings did not match the standard, live-follow, auto-collapse experience.

Root cause:
Backend and frontend fallback paths, browser mocks, and test fixtures retained the previous summary and compact defaults.

Fix:
Resolve missing or unknown reasoning modes to auto while preserving explicit selections and legacy localStorage migration. Align startup and browser defaults, and add config/Wails persistence coverage.

Verification:
- go test ./internal/config
- cd desktop && go test ./...
- cd desktop/frontend && pnpm test:typecheck
- cd desktop/frontend && pnpm test
- cd desktop/frontend && pnpm build
- go run ./tools/repolint
- git diff --check
This commit is contained in:
SivanCola committed 2026-08-10 17:38:09 +08:00
1 parent c75a977215
commit 70dc3ba58d
11 files changed
+88 -34

No files matched your search

+1 -1
View File
@@ -1563,7 +1563,7 @@ export default function App() {
void syncDesktopPreferences().catch((e) => {
console.warn("desktop preferences sync failed", e);
setStartupUpdateChecksEnabled(true);
hydrateReasoningDisplayMode("summary", false);
hydrateReasoningDisplayMode("auto", false);
});
return () => {
cancelled = true;
@@ -55,6 +55,10 @@ const rootEl = document.getElementById("root");
if (!rootEl) throw new Error("missing root");
const root = createRoot(rootEl);
// Most assertions in this file exercise the summary-mode disclosure behavior.
// Select it explicitly so the test remains independent from the product default.
hydrateReasoningDisplayMode("summary", true);
type ReasoningItem = React.ComponentProps<typeof AssistantMessage>["item"];
async function render(item: ReasoningItem, props: { defaultExpanded?: boolean } = {}) {
@@ -32,11 +32,14 @@ Object.defineProperty(globalThis, "localStorage", {
});
storage.clear();
hydrateReasoningDisplayMode("summary", false);
ok(getReasoningDisplayMode() === "summary", "missing preferences default to summary");
hydrateReasoningDisplayMode(undefined, false);
ok(getReasoningDisplayMode() === "auto", "missing preferences default to live follow");
hydrateReasoningDisplayMode("future-mode", false);
ok(getReasoningDisplayMode() === "auto", "unknown preferences use the live-follow fallback");
storage.set("reasonix-reasoning-summary", "0");
hydrateReasoningDisplayMode("summary", false);
hydrateReasoningDisplayMode("auto", false);
ok(getReasoningDisplayMode() === "legacy-collapsed", "legacy summary-off preserves the old compatible behavior");
storage.set("reasonix-reasoning-summary", "1");
@@ -47,6 +50,9 @@ storage.set("reasonix-reasoning-summary", "0");
hydrateReasoningDisplayMode("hidden", true);
ok(getReasoningDisplayMode() === "hidden", "explicit new mode wins over every legacy preference");
hydrateReasoningDisplayMode("summary", true);
ok(getReasoningDisplayMode() === "summary", "an explicit summary selection remains persisted");
storage.set("reasonix-reasoning-summary", "0");
ok(resolveReasoningDisplayMode("auto", false) === "legacy-collapsed", "resolver applies legacy precedence without hydration");
hydrateReasoningDisplayMode("auto", false);
@@ -46,9 +46,13 @@ ok(
"App loads startup chrome preferences through the lightweight settings call",
);
ok(
appSource.includes('hydrateReasoningDisplayMode("summary", false);'),
appSource.includes('hydrateReasoningDisplayMode("auto", false);'),
"startup failure preserves legacy reasoning-display migration precedence",
);
ok(
bridgeSource.includes('displayMode: "standard", reasoningDisplayMode: "auto", reasoningDisplayModeExplicit: false'),
"browser startup defaults match the classic standard/live-follow experience",
);
ok(
!/const\s+reloadSidebarImConnections[\s\S]*?app\.Settings\(\)[\s\S]*?\}, \[t\]\);/.test(appSource),
"sidebar IM refresh avoids rebuilding the full Settings payload",
+1 -1
View File
@@ -1723,7 +1723,7 @@ function makeMockApp(): AppBindings {
desktopTerminalTheme: "auto",
conversationWidth: "standard",
closeBehavior: "background",
displayMode: "compact", reasoningDisplayMode: "summary", reasoningDisplayModeExplicit: false,
displayMode: "standard", reasoningDisplayMode: "auto", reasoningDisplayModeExplicit: false,
statusBarStyle: "text",
statusBarItems: [...DEFAULT_STATUS_BAR_ITEMS],
defaultToolApprovalMode: "auto",
@@ -6,7 +6,7 @@ export type ResolvedReasoningDisplayMode = ReasoningDisplayMode | "legacy-collap
const LEGACY_SUMMARY_KEY = "reasonix-reasoning-summary";
const DISPLAY_EVENT = "reasonix:reasoning-display-mode";
let currentMode: ResolvedReasoningDisplayMode = "summary";
let currentMode: ResolvedReasoningDisplayMode = "auto";
let currentModeExplicit = false;
const listeners = new Set<() => void>();
@@ -39,11 +39,15 @@ export function resolveReasoningDisplayMode(
case "on":
return "summary";
}
return normalized ?? "summary";
return normalized ?? "auto";
}
export function getReasoningDisplayMode(): ResolvedReasoningDisplayMode {
if (!currentModeExplicit && currentMode === "summary" && legacySummaryValue() === "off") return "legacy-collapsed";
if (!currentModeExplicit && currentMode !== "pending") {
const legacy = legacySummaryValue();
if (legacy === "off") return "legacy-collapsed";
if (legacy === "on") return "summary";
}
return currentMode;
}
@@ -96,7 +96,7 @@ export function baseSettings(displayMode: "standard" | "compact" = "standard"):
desktopTerminalTheme: "auto",
closeBehavior: "background",
displayMode,
reasoningDisplayMode: "summary",
reasoningDisplayMode: "auto",
reasoningDisplayModeExplicit: false,
statusBarStyle: "text",
statusBarItems: ["model", "workspace", "git_branch", "cache", "balance"],
+2 -2
View File
@@ -12,7 +12,7 @@ func desktopStartupSettingsFromConfig(cfg *config.Config) DesktopStartupSettings
return DesktopStartupSettingsView{
Bot: botSettingsView(config.BotConfig{}), DesktopLayoutStyle: "workbench",
DesktopTheme: "auto", DesktopThemeStyle: "graphite", DesktopTerminalTheme: "auto",
DisplayMode: "standard", ReasoningDisplayMode: "summary", StatusBarStyle: "text",
DisplayMode: "standard", ReasoningDisplayMode: "auto", StatusBarStyle: "text",
StatusBarItems: config.DefaultDesktopStatusBarItems(), CheckUpdates: true,
UpdateChannel: "stable", ConversationWidth: "standard",
}
@@ -45,7 +45,7 @@ func (a *App) defaultSettingsView() SettingsView {
},
Bot: botSettingsView(config.BotConfig{}), AutoPlan: "off", DesktopLayoutStyle: "workbench",
DesktopTheme: "auto", DesktopThemeStyle: "graphite", DesktopTerminalTheme: "auto",
CloseBehavior: "background", DisplayMode: "standard", ReasoningDisplayMode: "summary",
CloseBehavior: "background", DisplayMode: "standard", ReasoningDisplayMode: "auto",
StatusBarStyle: "text", StatusBarItems: config.DefaultDesktopStatusBarItems(),
DefaultToolApprovalMode: "auto", CheckUpdates: true, UpdateChannel: "stable",
Telemetry: true, Metrics: true, ExpandThinking: false, ConversationWidth: "standard",
+37
View File
@@ -0,0 +1,37 @@
package main
import (
"testing"
"reasonix/internal/config"
)
func TestReasoningDisplayDefaultsAndPersistsUserSelection(t *testing.T) {
isolateDesktopUserDirs(t)
app := NewApp()
settings := app.Settings()
startup := app.DesktopStartupSettings()
if settings.DisplayMode != "standard" || settings.ReasoningDisplayMode != "auto" || settings.ReasoningDisplayModeExplicit {
t.Fatalf("Settings() defaults = display:%q reasoning:%q explicit:%t, want standard/auto/false", settings.DisplayMode, settings.ReasoningDisplayMode, settings.ReasoningDisplayModeExplicit)
}
if startup.DisplayMode != "standard" || startup.ReasoningDisplayMode != "auto" || startup.ReasoningDisplayModeExplicit {
t.Fatalf("DesktopStartupSettings() defaults = display:%q reasoning:%q explicit:%t, want standard/auto/false", startup.DisplayMode, startup.ReasoningDisplayMode, startup.ReasoningDisplayModeExplicit)
}
if err := app.SetReasoningDisplayMode("summary"); err != nil {
t.Fatalf("SetReasoningDisplayMode(summary): %v", err)
}
settings = app.Settings()
startup = app.DesktopStartupSettings()
if settings.ReasoningDisplayMode != "summary" || !settings.ReasoningDisplayModeExplicit {
t.Fatalf("Settings() user selection = reasoning:%q explicit:%t, want summary/true", settings.ReasoningDisplayMode, settings.ReasoningDisplayModeExplicit)
}
if startup.ReasoningDisplayMode != "summary" || !startup.ReasoningDisplayModeExplicit {
t.Fatalf("DesktopStartupSettings() user selection = reasoning:%q explicit:%t, want summary/true", startup.ReasoningDisplayMode, startup.ReasoningDisplayModeExplicit)
}
persisted := config.LoadForEdit(config.UserConfigPath())
if persisted.DesktopReasoningDisplayMode() != "summary" || !persisted.DesktopReasoningDisplayModeExplicit() {
t.Fatalf("persisted user selection = %+v, want explicit summary", persisted.Desktop)
}
}
+4 -14
View File
@@ -12,13 +12,9 @@ func (c *Config) DesktopReasoningDisplayMode() string {
case "hidden", "summary", "auto":
return raw
}
if raw != "" {
return "summary"
}
if c.Desktop.ExpandThinking {
return "auto"
}
return "summary"
// Missing and unknown values use the classic live-follow behavior. A user
// selection is persisted as a valid enum and therefore returns above.
return "auto"
}
// DesktopReasoningDisplayModeExplicit reports whether a valid new enum was stored.
@@ -55,13 +51,7 @@ func (c *Config) SetExpandThinking(on bool) error {
}
func renderDesktopReasoningDisplayMode(b *strings.Builder, c *Config) {
legacyExpand := c.Desktop.ExpandThinking
if strings.TrimSpace(c.Desktop.ReasoningDisplayMode) != "" && !c.DesktopReasoningDisplayModeExplicit() {
// Unknown future/invalid enums read as summary. When another setting is
// saved, keep that safe fallback stable after the invalid field is omitted.
legacyExpand = false
}
fmt.Fprintf(b, "expand_thinking = %v # desktop: legacy reasoning display alias; use reasoning_display_mode\n", legacyExpand)
fmt.Fprintf(b, "expand_thinking = %v # desktop: legacy reasoning display alias; use reasoning_display_mode\n", c.Desktop.ExpandThinking)
if c.DesktopReasoningDisplayModeExplicit() {
fmt.Fprintf(b, "reasoning_display_mode = %q # desktop: hidden|summary|auto reasoning presentation\n", c.DesktopReasoningDisplayMode())
}
+17 -8
View File
@@ -15,13 +15,14 @@ func TestDesktopReasoningDisplayModeDefaultsAndLegacy(t *testing.T) {
want string
explicit bool
}{
{name: "missing defaults to summary", want: "summary"},
{name: "missing defaults to live follow", want: "auto"},
{name: "legacy expand false follows the new default", expand: false, want: "auto"},
{name: "legacy expand true maps to auto", expand: true, want: "auto"},
{name: "explicit hidden wins over legacy", mode: "hidden", expand: true, want: "hidden", explicit: true},
{name: "explicit summary wins over legacy", mode: "summary", expand: true, want: "summary", explicit: true},
{name: "explicit auto", mode: "auto", want: "auto", explicit: true},
{name: "unknown safely falls back", mode: "future-mode", want: "summary"},
{name: "unknown ignores legacy expand", mode: "future-mode", expand: true, want: "summary"},
{name: "unknown follows the live-follow fallback", mode: "future-mode", want: "auto"},
{name: "unknown with legacy expand remains auto", mode: "future-mode", expand: true, want: "auto"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -79,9 +80,17 @@ func TestSetDesktopReasoningDisplayMode(t *testing.T) {
func TestRenderTOMLIncludesExplicitReasoningDisplayMode(t *testing.T) {
cfg := Default()
if got := RenderTOML(cfg); strings.Contains(got, "reasoning_display_mode =") {
renderedDefault := RenderTOML(cfg)
if strings.Contains(renderedDefault, "reasoning_display_mode =") {
t.Fatal("implicit default unexpectedly rendered reasoning_display_mode")
}
var decodedDefault Config
if _, err := toml.Decode(renderedDefault, &decodedDefault); err != nil {
t.Fatalf("decode rendered default config: %v", err)
}
if got := decodedDefault.DesktopReasoningDisplayMode(); got != "auto" {
t.Fatalf("round-tripped implicit default mode = %q, want auto", got)
}
if err := cfg.SetDesktopReasoningDisplayMode("hidden"); err != nil {
t.Fatalf("SetDesktopReasoningDisplayMode: %v", err)
}
@@ -102,8 +111,8 @@ func TestRenderTOMLOmitsUnknownReasoningDisplayMode(t *testing.T) {
if strings.Contains(rendered, "reasoning_display_mode =") {
t.Fatalf("unknown reasoning display mode was canonicalized into an explicit preference:\n%s", rendered)
}
if !strings.Contains(rendered, "expand_thinking = false") {
t.Fatalf("unknown mode was not normalized to the safe legacy summary alias:\n%s", rendered)
if !strings.Contains(rendered, "expand_thinking = true") {
t.Fatalf("unknown mode did not preserve the legacy compatibility alias:\n%s", rendered)
}
var decoded Config
if _, err := toml.Decode(rendered, &decoded); err != nil {
@@ -112,8 +121,8 @@ func TestRenderTOMLOmitsUnknownReasoningDisplayMode(t *testing.T) {
if decoded.DesktopReasoningDisplayModeExplicit() {
t.Fatalf("unknown mode became explicit after round trip: %+v", decoded.Desktop)
}
if got := decoded.DesktopReasoningDisplayMode(); got != "summary" {
t.Fatalf("round-tripped unknown mode = %q, want stable summary fallback", got)
if got := decoded.DesktopReasoningDisplayMode(); got != "auto" {
t.Fatalf("round-tripped unknown mode = %q, want stable live-follow fallback", got)
}
}