From e0dbdc56d603ed66bcba5f9c587999d38b0dde45 Mon Sep 17 00:00:00 2001 From: ferologics Date: Wed, 7 Jan 2026 11:31:14 +0100 Subject: [PATCH] fix: preserve externally-added settings when saving When a user edits settings.json while pi is running (e.g., adding enabledModels), those settings would be lost when pi saved other changes (e.g., changing thinking level via Shift+Tab). The fix re-reads the file before saving and merges the current file contents with in-memory changes, so external edits are preserved. Adds test coverage for SettingsManager. --- .../coding-agent/src/core/settings-manager.ts | 8 +- .../test/settings-manager.test.ts | 108 ++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 packages/coding-agent/test/settings-manager.test.ts diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index e9f29b5d..aae42ac5 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -178,7 +178,13 @@ export class SettingsManager { mkdirSync(dir, { recursive: true }); } - // Save only global settings (project settings are read-only) + // Re-read current file to preserve any settings added externally while running + const currentFileSettings = SettingsManager.loadFromFile(this.settingsPath); + // Merge: file settings as base, globalSettings (in-memory changes) as overrides + const mergedSettings = deepMergeSettings(currentFileSettings, this.globalSettings); + this.globalSettings = mergedSettings; + + // Save merged settings (project settings are read-only) writeFileSync(this.settingsPath, JSON.stringify(this.globalSettings, null, 2), "utf-8"); } catch (error) { console.error(`Warning: Could not save settings file: ${error}`); diff --git a/packages/coding-agent/test/settings-manager.test.ts b/packages/coding-agent/test/settings-manager.test.ts new file mode 100644 index 00000000..ad7fb0f3 --- /dev/null +++ b/packages/coding-agent/test/settings-manager.test.ts @@ -0,0 +1,108 @@ +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs"; +import { join } from "path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { SettingsManager } from "../src/core/settings-manager.js"; + +describe("SettingsManager", () => { + const testDir = join(process.cwd(), "test-settings-tmp"); + const agentDir = join(testDir, "agent"); + const projectDir = join(testDir, "project"); + + beforeEach(() => { + // Clean up and create fresh directories + if (existsSync(testDir)) { + rmSync(testDir, { recursive: true }); + } + mkdirSync(agentDir, { recursive: true }); + mkdirSync(join(projectDir, ".pi"), { recursive: true }); + }); + + afterEach(() => { + if (existsSync(testDir)) { + rmSync(testDir, { recursive: true }); + } + }); + + describe("preserves externally added settings", () => { + it("should preserve enabledModels when changing thinking level", () => { + // Create initial settings file + const settingsPath = join(agentDir, "settings.json"); + writeFileSync( + settingsPath, + JSON.stringify({ + theme: "dark", + defaultModel: "claude-sonnet", + }), + ); + + // Create SettingsManager (simulates pi starting up) + const manager = SettingsManager.create(projectDir, agentDir); + + // Simulate user editing settings.json externally to add enabledModels + const currentSettings = JSON.parse(readFileSync(settingsPath, "utf-8")); + currentSettings.enabledModels = ["claude-opus-4-5", "gpt-5.2-codex"]; + writeFileSync(settingsPath, JSON.stringify(currentSettings, null, 2)); + + // User changes thinking level via Shift+Tab + manager.setDefaultThinkingLevel("high"); + + // Verify enabledModels is preserved + const savedSettings = JSON.parse(readFileSync(settingsPath, "utf-8")); + expect(savedSettings.enabledModels).toEqual(["claude-opus-4-5", "gpt-5.2-codex"]); + expect(savedSettings.defaultThinkingLevel).toBe("high"); + expect(savedSettings.theme).toBe("dark"); + expect(savedSettings.defaultModel).toBe("claude-sonnet"); + }); + + it("should preserve custom settings when changing theme", () => { + const settingsPath = join(agentDir, "settings.json"); + writeFileSync( + settingsPath, + JSON.stringify({ + defaultModel: "claude-sonnet", + }), + ); + + const manager = SettingsManager.create(projectDir, agentDir); + + // User adds custom settings externally + const currentSettings = JSON.parse(readFileSync(settingsPath, "utf-8")); + currentSettings.shellPath = "/bin/zsh"; + currentSettings.extensions = ["/path/to/extension.ts"]; + writeFileSync(settingsPath, JSON.stringify(currentSettings, null, 2)); + + // User changes theme + manager.setTheme("light"); + + // Verify all settings preserved + const savedSettings = JSON.parse(readFileSync(settingsPath, "utf-8")); + expect(savedSettings.shellPath).toBe("/bin/zsh"); + expect(savedSettings.extensions).toEqual(["/path/to/extension.ts"]); + expect(savedSettings.theme).toBe("light"); + }); + + it("should let in-memory changes override file changes for same key", () => { + const settingsPath = join(agentDir, "settings.json"); + writeFileSync( + settingsPath, + JSON.stringify({ + theme: "dark", + }), + ); + + const manager = SettingsManager.create(projectDir, agentDir); + + // User externally sets thinking level to "low" + const currentSettings = JSON.parse(readFileSync(settingsPath, "utf-8")); + currentSettings.defaultThinkingLevel = "low"; + writeFileSync(settingsPath, JSON.stringify(currentSettings, null, 2)); + + // But then changes it via UI to "high" + manager.setDefaultThinkingLevel("high"); + + // In-memory change should win + const savedSettings = JSON.parse(readFileSync(settingsPath, "utf-8")); + expect(savedSettings.defaultThinkingLevel).toBe("high"); + }); + }); +});