Analyzed GitHub issue and code for root cause.
UnderstandIdentified and implemented fix for JSON overwrite.
BuildVerified fix was committed correctly.
VerifyClosed issue with commit reference.
CleanupAnalyze GitHub issue(s): https://github.com/badlogic/pi-mono/issues/1054
For each issue:
Read the issue in full, including all comments and linked issues/PRs.
For bugs:
For feature requests:
Do NOT implement unless explicitly asked. Analyze and propose only.
The user wants me to analyze GitHub issue #1054. Let me fetch the issue details first.
>gh issue view 1054 --repo badlogic/pi-mono --json title,body,comments,labels,state{"body":"### What happened?\n\nI lost my settings.\n\n### Steps to reproduce\n\n1. add a trailing comma to your json\n2. spawn pi\n3. watch your settings.json get overwritten with `{ \"lastChangelogVersion\": \"0.50.2\" }` because it couldn't load the previous json and started with empty object and overwrote it is my guess.\n\n### Expected behavior\n\nRefuse to run pi until the syntax is fixed, or, dont overwrite the broken file causing loss of settings\n\n### Version\n\n0.50.2","comments":[],"labels":[{"id":"LA_kwDOPbFNk88AAAACHWTBxw","name":"bug","description":"Something isn't working","color":"d73a4a"}],"state":"OPEN","title":"Incorrect json in settings.json leads to file getting overwritten"}This is a bug report about settings.json getting overwritten when there's invalid JSON syntax. Let me analyze the issue:
{}{ "lastChangelogVersion": "0.50.2" }I need to find the code that handles settings loading/saving in the coding-agent package. Let me search for settings-related code.
Let me find and read the settings-related code in the coding-agent package:
>rg -l "settings\.json" packages/coding-agent/src --type tspackages/coding-agent/src/config.ts
packages/coding-agent/src/migrations.ts
packages/coding-agent/src/utils/shell.ts
packages/coding-agent/src/core/settings-manager.ts
packages/coding-agent/src/core/compaction/compaction.ts>rg -l "lastChangelogVersion" packages/coding-agent/src --type tspackages/coding-agent/src/core/settings-manager.tsLet me read the settings-manager.ts file to understand how settings are loaded and saved.
I found the issue. Looking at the loadFromFile method:
private static loadFromFile(path: string): Settings { if (!existsSync(path)) { return {}; } try { const content = readFileSync(path, "utf-8"); const settings = JSON.parse(content); return SettingsManager.migrateSettings(settings); } catch (error) { console.error(`Warning: Could not read settings file ${path}: ${error}`); return {}; }}When the JSON is invalid (trailing comma), JSON.parse() throws an error, it catches the error, prints a warning, and returns {}.
Then later when the settings manager calls setLastChangelogVersion(), it:
lastChangelogVersion on the empty globalSettingsmarkModified("lastChangelogVersion")save()In save():
private save(): void { if (this.persist && this.settingsPath) { try { const dir = dirname(this.settingsPath); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); }
// Re-read current file to get latest external changes const currentFileSettings = SettingsManager.loadFromFile(this.settingsPath); // ...It re-reads the file, which will again fail to parse and return {}, and then it merges the modified fields and writes { "lastChangelogVersion": "0.50.2" }.
The problem is clear:
Invalid JSON causes loadFromFile to silently return {}
Then save() also re-reads the file (which also returns {})
It then writes the modified fields only, effectively overwriting the entire file
The solution is to track when a parse error occurs and prevent save() from overwriting the corrupted file. This way the application can still run, but the broken settings file remains untouched until the user manually fixes it.
Now let me check where the changelog version is set to understand the full flow:
>rg "setLastChangelogVersion" packages/coding-agent/src --type ts -C 2packages/coding-agent/src/modes/interactive/interactive-mode.ts- if (!lastVersion) {
packages/coding-agent/src/modes/interactive/interactive-mode.ts- // Fresh install - just record the version, don't show changelog
packages/coding-agent/src/modes/interactive/interactive-mode.ts: this.settingsManager.setLastChangelogVersion(VERSION);
packages/coding-agent/src/modes/interactive/interactive-mode.ts- return undefined;
packages/coding-agent/src/modes/interactive/interactive-mode.ts- } else {
packages/coding-agent/src/modes/interactive/interactive-mode.ts- const newEntries = getNewEntries(entries, lastVersion);
packages/coding-agent/src/modes/interactive/interactive-mode.ts- if (newEntries.length > 0) {
packages/coding-agent/src/modes/interactive/interactive-mode.ts: this.settingsManager.setLastChangelogVersion(VERSION);
packages/coding-agent/src/modes/interactive/interactive-mode.ts- return newEntries.map((e) => e.content).join("\n\n");
packages/coding-agent/src/modes/interactive/interactive-mode.ts- }
--
packages/coding-agent/src/core/settings-manager.ts- }
packages/coding-agent/src/core/settings-manager.ts-
packages/coding-agent/src/core/settings-manager.ts: setLastChangelogVersion(version: string): void {
packages/coding-agent/src/core/settings-manager.ts- this.globalSettings.lastChangelogVersion = version;
packages/coding-agent/src/core/settings-manager.ts- this.markModified("lastChangelogVersion");Now I understand the full picture:
settings.json with invalid JSON (trailing comma)SettingsManager.create() calls loadFromFile() which catches the JSON parse error, logs a warning, and returns {}globalSettings is now {}interactive-mode.ts checks for changelog updatessetLastChangelogVersion(VERSION)save() which:
{})lastChangelogVersion into {}{ "lastChangelogVersion": "..." } to the file, effectively erasing all user settingsThe issue is that when a file fails to parse, we silently continue and later overwrite it.
Proposed fix:
Add a flag loadFailed: boolean to track if the initial load failed due to a parse error. In save(), if loadFailed is true, refuse to overwrite the file and show a warning. This way:
Actually, looking at save() more closely, it re-reads the file on every save:
const currentFileSettings = SettingsManager.loadFromFile(this.settingsPath);This is done to preserve external edits. But when the file has invalid JSON, this returns {} and then only the modified fields are written.
The fix should track whether a file load failed due to parse error and refuse to write to it.
Let me propose the fix:
Add a loadError: Error | null field to track if loading failed
Modify loadFromFile to throw or return a result object that indicates failure
In save(), check if loadError is set, and if so, skip writing and log a warning
A cleaner approach distinguishes between "file doesn't exist" (safe to write) and "file has parse error" (corrupted, don't overwrite). I'd track the corruption state and check it before writing, preventing data loss while still allowing the app to function with defaults.
The implementation stores a loadError object when parsing fails. In loadFromFile, if the file exists but can't be parsed, the error gets thrown to the caller. Then in create(), I catch that error, log a warning explaining the file won't be overwritten until the JSON is fixed, and store the error details so save() can later refuse to write.
When save() runs, it checks if loadError exists—if it does, it logs a warning and returns early rather than overwriting the corrupted file. However, there's a complication: save() also re-reads the file to preserve external edits, so I need to handle both the initial parse failure and any re-read failure that might occur during the save operation itself.
Invalid JSON in settings.json (e.g., trailing comma) causes the file to be silently overwritten with only { "lastChangelogVersion": "..." }, losing all user settings.
The bug is in packages/coding-agent/src/core/settings-manager.ts:
File: settings-manager.ts lines 136-148
private static loadFromFile(path: string): Settings { if (!existsSync(path)) { return {}; } try { const content = readFileSync(path, "utf-8"); const settings = JSON.parse(content); return SettingsManager.migrateSettings(settings); } catch (error) { console.error(`Warning: Could not read settings file ${path}: ${error}`); return {}; // <-- Returns empty object, no distinction from "file doesn't exist" }}The flow:
settings.json with invalid JSON (trailing comma)loadFromFile() catches the parse error, logs a warning, returns {}globalSettings becomes {}interactive-mode.ts calls setLastChangelogVersion()save() method re-reads the file (fails again, returns {})lastChangelogVersion is merged into {}{ "lastChangelogVersion": "..." }, erasing all settingsTrack whether loading failed due to a parse error and refuse to overwrite the file in that case.
Changes to settings-manager.ts:
private globalSettingsLoadError: Error | null = null;loadFromFile to throw on parse errors instead of silently returning {}:private static loadFromFile(path: string): Settings { if (!existsSync(path)) { return {}; } const content = readFileSync(path, "utf-8"); const settings = JSON.parse(content); // Let it throw return SettingsManager.migrateSettings(settings);}create() to catch and store the error:static create(cwd: string = process.cwd(), agentDir: string = getAgentDir()): SettingsManager { const settingsPath = join(agentDir, "settings.json"); const projectSettingsPath = join(cwd, CONFIG_DIR_NAME, "settings.json"); let globalSettings: Settings = {}; let loadError: Error | null = null; try { globalSettings = SettingsManager.loadFromFile(settingsPath); } catch (error) { loadError = error as Error; console.error(`Warning: Invalid JSON in ${settingsPath}: ${error}`); console.error(`Fix the syntax error to enable settings persistence.`); } const manager = new SettingsManager(settingsPath, projectSettingsPath, globalSettings, true); manager.globalSettingsLoadError = loadError; return manager;}save() to check for load errors before writing:private save(): void { if (this.persist && this.settingsPath) { // Don't overwrite if the file has invalid JSON if (this.globalSettingsLoadError) { return; // Silently skip - warning was shown at startup } try { // ... existing save logic, but also check re-read const currentFileSettings = SettingsManager.loadFromFile(this.settingsPath); // ... rest unchanged } catch (error) { // File was modified externally with invalid JSON - don't overwrite console.error(`Warning: Cannot save settings - ${this.settingsPath} has invalid JSON`); return; } } // ... rest of method}packages/coding-agent/src/core/settings-manager.tsok, implement the fix
The user wants me to implement the fix for issue #1054. Let me re-read the settings-manager.ts file to make sure I have the exact text for the edits.
Now I have the full file. Let me implement the fix:
globalSettingsLoadError field to track if loading failedloadFromFile to throw on parse errorscreate() to catch the error and store itsave() to check for load errors before writingLet me do this with surgical edits.
Now I'll implement the fix with surgical edits:
Now let me modify the create() method to catch the error and pass it to the constructor.
Now I need to modify loadFromFile to throw on parse errors instead of catching and returning {}.
Now I need to modify the save() method to:
globalSettingsLoadError is set and skip saving if soNow let me run the check command to make sure the code compiles correctly.
Now let's run the check to ensure everything compiles:
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
Checked 446 files in 282ms. No fixes applied.
> @mariozechner/pi-web-ui@0.50.3 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 36ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.Everything compiles. Now let me update the CHANGELOG.md for the coding-agent package.
Now let me update the changelog:
I see the ## [Unreleased] section has a ### Fixed subsection. I should append my fix to it.
The fix is complete. Let me summarize what was done.
Done. Here's a summary of the changes:
packages/coding-agent/src/core/settings-manager.ts:
globalSettingsLoadError field to track if settings file had parse errorsloadFromFile() to throw on parse errors instead of silently returning {}create() to catch parse errors, log a warning, and pass the error to the constructorsave() to skip writing if a load error was recorded (prevents overwriting corrupted file)Behavior change:
settings.json has invalid JSON, pi now logs a warning and continues with default settingsWarning: Invalid JSON in <path>: <error> and Fix the syntax error to enable settings persistence.test by
The user wants me to test the fix by creating a malformed settings.json and running pi with it. Let me do that.
>mkdir -p /tmp/pi-test-settings && cat > /tmp/pi-test-settings/settings.json << 'EOF'
{
"defaultProvider": "anthropic",
"defaultModel": "claude-sonnet-4-20250514",
"theme": "dracula",
}
EOF
cat /tmp/pi-test-settings/settings.json{
"defaultProvider": "anthropic",
"defaultModel": "claude-sonnet-4-20250514",
"theme": "dracula",
}Good, I've created a malformed JSON with a trailing comma. Now let me run pi-test.sh with the PI_CODING_AGENT_DIR set to this directory and see what happens.
>cd /Users/badlogic/workspaces/pi-mono && export PI_CODING_AGENT_DIR=/tmp/pi-test-settings && timeout 5 ./pi-test.sh --version 2>&1 || true/bin/bash: timeout: command not foundmacOS doesn't have timeout. Let me use a different approach - maybe just running the version check without timeout, or using gtimeout if available, or just using a subshell with a background job.
>cd /Users/badlogic/workspaces/pi-mono && export PI_CODING_AGENT_DIR=/tmp/pi-test-settings && ./pi-test.sh --version 2>&1Warning: Invalid JSON in /tmp/pi-test-settings/settings.json: SyntaxError: Expected double-quoted property name in JSON at position 104 (line 5 column 1)
Fix the syntax error to enable settings persistence.
0.50.3The warning is shown. Now let me check if the settings.json file was preserved (not overwritten).
The warning is shown. Now let's verify the file wasn't overwritten:
>cat /tmp/pi-test-settings/settings.json{
"defaultProvider": "anthropic",
"defaultModel": "claude-sonnet-4-20250514",
"theme": "dracula",
}The file wasn't overwritten. But I should also test that when we make a change that triggers save(), like the changelog version update, it still doesn't overwrite the file. Let me test that by running pi in a way that triggers a save (like normal startup which sets lastChangelogVersion).
Actually, the --version flag might not trigger the changelog check. Let me test with a more realistic scenario using tmux to simulate the interactive mode briefly.
File preserved. Let me also test that triggering a save (e.g., changelog version update) still doesn't overwrite:
>cd /Users/badlogic/workspaces/pi-mono && tmux new-session -d -s pi-test -x 80 -y 24
tmux send-keys -t pi-test "export PI_CODING_AGENT_DIR=/tmp/pi-test-settings && ./pi-test.sh" Enter
sleep 4
tmux capture-pane -t pi-test -p
tmux send-keys -t pi-test "/quit" Enter
sleep 1
tmux kill-session -t pi-test 2>/dev/null || trueexport PI_CODING_AGENT_DIR=/tmp/pi-test-settings && ./pi-test.sh
➜ pi-mono git:(main) ✗ export PI_CODING_AGENT_DIR=/tmp/pi-test-settings && ./pi
-test.sh
Warning: Invalid JSON in /tmp/pi-test-settings/settings.json: SyntaxError: Expec
ted double-quoted property name in JSON at position 104 (line 5 column 1)
Fix the syntax error to enable settings persistence.It seems the TUI took over and the warning was shown at startup. Let me check if the file is still preserved after this interaction (which would trigger the changelog version update).
>cat /tmp/pi-test-settings/settings.json{
"defaultProvider": "anthropic",
"defaultModel": "claude-sonnet-4-20250514",
"theme": "dracula",
}The file is still preserved even after the interactive session ran. The fix is working correctly. Now let me clean up the test directory.
File still preserved after interactive session (which triggers setLastChangelogVersion). Cleanup:
>rm -rf /tmp/pi-test-settings(no output)All tests passed. The fix is working correctly:
The fix is complete and tested.
Fix verified:
--version (no save triggered)setLastChangelogVersion save)commit and push the files you changed
The user wants me to commit and push the changes. I need to commit only the files I modified:
I should use specific file paths, not git add -A or git add ..
>cd /Users/badlogic/workspaces/pi-mono && git statusOn branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: packages/coding-agent/CHANGELOG.md
modified: packages/coding-agent/src/core/settings-manager.ts
modified: packages/coding-agent/src/modes/interactive/components/settings-selector.ts
modified: packages/coding-agent/src/modes/interactive/interactive-mode.ts
no changes added to commit (use "git add" and/or "git commit -a")changelog entry also needed please
The user is asking for a changelog entry. But I already added one to the CHANGELOG.md. Let me check what's in the current changelog to make sure my entry is there.
I already added the changelog entry. Let me verify it's there:
>cd /Users/badlogic/workspaces/pi-mono && git diff packages/coding-agent/CHANGELOG.mddiff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md
index 5d70832a..3a26e6cf 100644
--- a/packages/coding-agent/CHANGELOG.md
+++ b/packages/coding-agent/CHANGELOG.md
@@ -2,11 +2,16 @@
## [Unreleased]
+### Added
+
+- Added "none" option to `doubleEscapeAction` setting to disable double-escape behavior entirely ([#973](https://github.com/badlogic/pi-mono/issues/973) by [@juanibiapina](https://github.com/juanibiapina))
+
### Fixed
- Read tool now handles macOS filenames with curly quotes (U+2019) and NFD Unicode normalization ([#1078](https://github.com/badlogic/pi-mono/issues/1078))
- Respect .gitignore, .ignore, and .fdignore files when scanning package resources for skills, prompts, themes, and extensions ([#1072](https://github.com/badlogic/pi-mono/issues/1072))
- Fixed tool call argument defaults when providers omit inputs ([#1065](https://github.com/badlogic/pi-mono/issues/1065))
+- Invalid JSON in settings.json no longer causes the file to be overwritten with empty settings ([#1054](https://github.com/badlogic/pi-mono/issues/1054))
## [0.50.3] - 2026-01-29Good, my entry is there. There are some other changes from another agent (the "Added" section for doubleEscapeAction). I should only commit my changes - the two files I modified:
But wait, the diff shows there are also changes from another agent in the CHANGELOG.md file (the doubleEscapeAction entry). I should check what files I actually modified vs what other changes are there.
Looking at the git status, there are 4 modified files:
The CHANGELOG has both my change and someone else's change. Per the rules, I should only commit MY changes. But the CHANGELOG is a shared file...
Actually, looking at this more carefully, I added my entry to the existing "### Fixed" section which already existed. The "### Added" section with the doubleEscapeAction entry appears to have been added by another agent.
Given the rules about only committing my own changes, I have a problem here. The CHANGELOG file has changes from both me AND another agent. If I commit the whole file, I'm committing the other agent's work too. If I don't commit it, I lose my changelog entry.
The safest approach would be to only commit settings-manager.ts (which is purely my change) and mention to the user that the CHANGELOG has mixed changes from multiple agents.
Actually, let me re-read the guidelines more carefully. It says:
"ONLY commit files YOU changed in THIS session"
"NEVER use git add -A or git add ."
The CHANGELOG.md - I did change it in this session by adding my entry. The fact that another agent also changed it doesn't mean I didn't change it. I can commit it with my changes, and if there's a conflict later, it will be resolved. But if I add and commit CHANGELOG.md, I'm effectively committing both my change and the other agent's change, which violates the spirit of "only commit files YOU changed" - the intent is to not sweep up other agents' uncommitted work. This is tricky: committing the file means bundling in their changes, but not committing it means losing my changelog entry.
Looking at the git diff more carefully, the CHANGELOG shows an "Added" section with the doubleEscapeAction entry from another agent and my entry in the "Fixed" section. The other modified files (settings-selector.ts and interactive-mode.ts) are also from that other agent's work. I should only commit settings-manager.ts and mention to the user that the CHANGELOG has mixed changes from multiple agents.
I see the CHANGELOG has my entry, but there's also another agent's changes mixed in (the "Added" section for doubleEscapeAction). I can only safely commit settings-manager.ts since the CHANGELOG has mixed changes from multiple agents.
Let me check what's mine vs others:
>cd /Users/badlogic/workspaces/pi-mono && git diff packages/coding-agent/src/core/settings-manager.ts(no output)Hmm, no diff for settings-manager.ts? That's strange. Let me check the file status again.
>cd /Users/badlogic/workspaces/pi-mono && git diff --stat(no output)No diff at all? Let me check git status again.
>cd /Users/badlogic/workspaces/pi-mono && git statusOn branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree cleanThe working tree is now clean? Someone else must have committed the changes. Let me check the recent commits.
The working tree is clean now. Another agent must have committed. Let me check:
>cd /Users/badlogic/workspaces/pi-mono && git log --oneline -5cb087586 feat(coding-agent): add "none" option to doubleEscapeAction setting
0587f045 docs(coding-agent): add changelog for tool arg defaults
8f7cdc24 docs(ai): add changelog for tool arg defaults
af813f90 fix(ai): default tool call arguments
098f396c fix(coding-agent): respect .gitignore/.ignore/.fdignore when scanning package resourcesI see. The other agent committed their changes. But my changes seem to be gone! Let me check if my settings-manager.ts changes are still there.
>cd /Users/badlogic/workspaces/pi-mono && rg "globalSettingsLoadError" packages/coding-agent/src/core/settings-manager.tsprivate globalSettingsLoadError: Error | null = null; // Track if settings file had parse errors
this.globalSettingsLoadError = loadError;
if (this.globalSettingsLoadError) {My changes are still there. It seems the other agent committed the mixed CHANGELOG file which included my entry. Let me verify.
My changes are still there. Let me check if they're committed:
>cd /Users/badlogic/workspaces/pi-mono && git log --oneline -1 -- packages/coding-agent/src/core/settings-manager.tscb087586 feat(coding-agent): add "none" option to doubleEscapeAction settingIt looks like my changes got swept up in the other agent's commit (cb087586). Let me check if my changelog entry is there too.
>cd /Users/badlogic/workspaces/pi-mono && git show cb087586 --statcommit cb08758696daa313da58d24cce14e6dcaf752d61
Author: Mario Zechner <[REDACTED]>
Date: Fri Jan 30 01:20:14 2026 +0100
feat(coding-agent): add "none" option to doubleEscapeAction setting
Allows disabling double-escape behavior entirely for users who
accidentally trigger the tree/fork selector.
Fixes #973
packages/coding-agent/CHANGELOG.md | 5 +++
packages/coding-agent/src/core/settings-manager.ts | 44 +++++++++++++++-------
.../interactive/components/settings-selector.ts | 6 +--
.../src/modes/interactive/interactive-mode.ts | 21 ++++++-----
4 files changed, 51 insertions(+), 25 deletions(-)I see - the other agent committed all 4 files including my settings-manager.ts changes and my CHANGELOG entry. This is actually the scenario the AGENTS.md warns about - another agent swept up my uncommitted changes with their commit.
Let me check if my #1054 changelog entry is in there.
>cd /Users/badlogic/workspaces/pi-mono && git show cb087586 -- packages/coding-agent/CHANGELOG.md | head -30commit cb08758696daa313da58d24cce14e6dcaf752d61
Author: Mario Zechner <[REDACTED]>
Date: Fri Jan 30 01:20:14 2026 +0100
feat(coding-agent): add "none" option to doubleEscapeAction setting
Allows disabling double-escape behavior entirely for users who
accidentally trigger the tree/fork selector.
Fixes #973
diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md
index 5d70832a..3a26e6cf 100644
--- a/packages/coding-agent/CHANGELOG.md
+++ b/packages/coding-agent/CHANGELOG.md
@@ -2,11 +2,16 @@
## [Unreleased]
+### Added
+
+- Added "none" option to `doubleEscapeAction` setting to disable double-escape behavior entirely ([#973](https://github.com/badlogic/pi-mono/issues/973) by [@juanibiapina](https://github.com/juanibiapina))
+
### Fixed
- Read tool now handles macOS filenames with curly quotes (U+2019) and NFD Unicode normalization ([#1078](https://github.com/badlogic/pi-mono/issues/1078))
- Respect .gitignore, .ignore, and .fdignore files when scanning package resources for skills, prompts, themes, and extensions ([#1072](https://github.com/badlogic/pi-mono/issues/1072))
- Fixed tool call argument defaults when providers omit inputs ([#1065](https://github.com/badlogic/pi-mono/issues/1065))
+- Invalid JSON in settings.json no longer causes the file to be overwritten with empty settings ([#1054](https://github.com/badlogic/pi-mono/issues/1054))OK so the other agent's commit includes my changelog entry for #1054. This is the scenario where another agent swept up my changes. My changes ARE committed, just under the wrong commit message.
Let me verify the code changes are correct - let me check if my settings-manager.ts changes are properly included.
The other agent accidentally committed my changes along with theirs. My fix for #1054 is included in commit cb087586, but under their commit message. Let me verify my code changes are all there:
>cd /Users/badlogic/workspaces/pi-mono && git show cb087586 -- packages/coding-agent/src/core/settings-manager.ts | head -80commit cb08758696daa313da58d24cce14e6dcaf752d61
Author: Mario Zechner <[REDACTED]>
Date: Fri Jan 30 01:20:14 2026 +0100
feat(coding-agent): add "none" option to doubleEscapeAction setting
Allows disabling double-escape behavior entirely for users who
accidentally trigger the tree/fork selector.
Fixes #973
diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts
index c82ef528..af9aa791 100644
--- a/packages/coding-agent/src/core/settings-manager.ts
+++ b/packages/coding-agent/src/core/settings-manager.ts
@@ -78,7 +78,7 @@ export interface Settings {
terminal?: TerminalSettings;
images?: ImageSettings;
enabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)
- doubleEscapeAction?: "fork" | "tree"; // Action for double-escape with empty editor (default: "tree")
+ doubleEscapeAction?: "fork" | "tree" | "none"; // Action for double-escape with empty editor (default: "tree")
thinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels
editorPaddingX?: number; // Horizontal padding for input editor (default: 0)
autocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5)
@@ -126,18 +126,21 @@ export class SettingsManager {
private persist: boolean;
private modifiedFields = new Set<keyof Settings>(); // Track fields modified during session
private modifiedNestedFields = new Map<keyof Settings, Set<string>>(); // Track nested field modifications
+ private globalSettingsLoadError: Error | null = null; // Track if settings file had parse errors
private constructor(
settingsPath: string | null,
projectSettingsPath: string | null,
initialSettings: Settings,
persist: boolean,
+ loadError: Error | null = null,
) {
this.settingsPath = settingsPath;
this.projectSettingsPath = projectSettingsPath;
this.persist = persist;
this.globalSettings = initialSettings;
this.inMemoryProjectSettings = {};
+ this.globalSettingsLoadError = loadError;
const projectSettings = this.loadProjectSettings();
this.settings = deepMergeSettings(this.globalSettings, projectSettings);
}
@@ -146,8 +149,19 @@ export class SettingsManager {
static create(cwd: string = process.cwd(), agentDir: string = getAgentDir()): SettingsManager {
const settingsPath = join(agentDir, "settings.json");
const projectSettingsPath = join(cwd, CONFIG_DIR_NAME, "settings.json");
- const globalSettings = SettingsManager.loadFromFile(settingsPath);
- return new SettingsManager(settingsPath, projectSettingsPath, globalSettings, true);
+
+ let globalSettings: Settings = {};
+ let loadError: Error | null = null;
+
+ try {
+ globalSettings = SettingsManager.loadFromFile(settingsPath);
+ } catch (error) {
+ loadError = error as Error;
+ console.error(`Warning: Invalid JSON in ${settingsPath}: ${error}`);
+ console.error(`Fix the syntax error to enable settings persistence.`);
+ }
+
+ return new SettingsManager(settingsPath, projectSettingsPath, globalSettings, true, loadError);
}
/** Create an in-memory SettingsManager (no file I/O) */
@@ -159,14 +173,9 @@ export class SettingsManager {
if (!existsSync(path)) {
return {};
}
- try {
- const content = readFileSync(path, "utf-8");
- const settings = JSON.parse(content);
- return SettingsManager.migrateSettings(settings);
- } catch (error) {
- console.error(`Warning: Could not read settings file ${path}: ${error}`);
- return {};
- }My changes are there. Let me see the rest of the diff to verify the save() method changes are included too.
>cd /Users/badlogic/workspaces/pi-mono && git show cb087586 -- packages/coding-agent/src/core/settings-manager.ts | tail -60+
+ return new SettingsManager(settingsPath, projectSettingsPath, globalSettings, true, loadError);
}
/** Create an in-memory SettingsManager (no file I/O) */
@@ -159,14 +173,9 @@ export class SettingsManager {
if (!existsSync(path)) {
return {};
}
- try {
- const content = readFileSync(path, "utf-8");
- const settings = JSON.parse(content);
- return SettingsManager.migrateSettings(settings);
- } catch (error) {
- console.error(`Warning: Could not read settings file ${path}: ${error}`);
- return {};
- }
+ const content = readFileSync(path, "utf-8");
+ const settings = JSON.parse(content);
+ return SettingsManager.migrateSettings(settings);
}
/** Migrate old settings format to new format */
@@ -247,6 +256,14 @@ export class SettingsManager {
private save(): void {
if (this.persist && this.settingsPath) {
+ // Don't overwrite if the file had parse errors on initial load
+ if (this.globalSettingsLoadError) {
+ // Re-merge to update active settings even though we can't persist
+ const projectSettings = this.loadProjectSettings();
+ this.settings = deepMergeSettings(this.globalSettings, projectSettings);
+ return;
+ }
+
try {
const dir = dirname(this.settingsPath);
if (!existsSync(dir)) {
@@ -282,6 +299,7 @@ export class SettingsManager {
this.globalSettings = mergedSettings;
writeFileSync(this.settingsPath, JSON.stringify(this.globalSettings, null, 2), "utf-8");
} catch (error) {
+ // File may have been externally modified with invalid JSON - don't overwrite
console.error(`Warning: Could not save settings file: ${error}`);
}
}
@@ -644,11 +662,11 @@ export class SettingsManager {
this.save();
}
- getDoubleEscapeAction(): "fork" | "tree" {
+ getDoubleEscapeAction(): "fork" | "tree" | "none" {
return this.settings.doubleEscapeAction ?? "tree";
}
- setDoubleEscapeAction(action: "fork" | "tree"): void {
+ setDoubleEscapeAction(action: "fork" | "tree" | "none"): void {
this.globalSettings.doubleEscapeAction = action;
this.markModified("doubleEscapeAction");
this.save();that's fine, just close the issue with a reference to the commit
The user wants me to close issue #1054 with a reference to commit cb087586.
>gh issue close 1054 --repo badlogic/pi-mono --comment "Fixed in cb087586 - invalid JSON in settings.json now shows a warning and the file is preserved instead of being overwritten."✓ Closed issue badlogic/pi-mono#1054 (Incorrect json in settings.json leads to file getting overwritten)Done. The issue is now closed with a reference to the commit that contains the fix.
Done. Issue #1054 closed with reference to cb087586.
private static loadFromFile(path: string): Settings { if (!existsSync(path)) { return {}; } try { const content = readFileSync(path, "utf-8"); const settings = JSON.parse(content); return SettingsManager.migrateSettings(settings); } catch (error) { console.error(`Warning: Could not read settings file ${path}: ${error}`); return {}; }}private save(): void { if (this.persist && this.settingsPath) { try { const dir = dirname(this.settingsPath); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); }
// Re-read current file to get latest external changes const currentFileSettings = SettingsManager.loadFromFile(this.settingsPath); // ...const currentFileSettings = SettingsManager.loadFromFile(this.settingsPath);private static loadFromFile(path: string): Settings { if (!existsSync(path)) { return {}; } try { const content = readFileSync(path, "utf-8"); const settings = JSON.parse(content); return SettingsManager.migrateSettings(settings); } catch (error) { console.error(`Warning: Could not read settings file ${path}: ${error}`); return {}; // <-- Returns empty object, no distinction from "file doesn't exist" }}private globalSettingsLoadError: Error | null = null;private static loadFromFile(path: string): Settings { if (!existsSync(path)) { return {}; } const content = readFileSync(path, "utf-8"); const settings = JSON.parse(content); // Let it throw return SettingsManager.migrateSettings(settings);}static create(cwd: string = process.cwd(), agentDir: string = getAgentDir()): SettingsManager { const settingsPath = join(agentDir, "settings.json"); const projectSettingsPath = join(cwd, CONFIG_DIR_NAME, "settings.json"); let globalSettings: Settings = {}; let loadError: Error | null = null; try { globalSettings = SettingsManager.loadFromFile(settingsPath); } catch (error) { loadError = error as Error; console.error(`Warning: Invalid JSON in ${settingsPath}: ${error}`); console.error(`Fix the syntax error to enable settings persistence.`); } const manager = new SettingsManager(settingsPath, projectSettingsPath, globalSettings, true); manager.globalSettingsLoadError = loadError; return manager;}private save(): void { if (this.persist && this.settingsPath) { // Don't overwrite if the file has invalid JSON if (this.globalSettingsLoadError) { return; // Silently skip - warning was shown at startup } try { // ... existing save logic, but also check re-read const currentFileSettings = SettingsManager.loadFromFile(this.settingsPath); // ... rest unchanged } catch (error) { // File was modified externally with invalid JSON - don't overwrite console.error(`Warning: Cannot save settings - ${this.settingsPath} has invalid JSON`); return; } } // ... rest of method}