@oh-my-pi/pi-coding-agent 8.13.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [9.0.0] - 2026-01-29
6
+
7
+ ### Fixed
8
+
9
+ - External edits to `config.yml` are now preserved when omp reloads or saves unrelated settings. Previously, editing config.yml directly (e.g., removing a package from `packages` array) would be silently reverted on next omp startup when automatic setters like `setLastChangelogVersion()` triggered a save. ([#1046](https://github.com/badlogic/pi-mono/pull/1046) by [@nicobailonMD](https://github.com/nicobailonMD))
10
+
5
11
  ## [8.13.0] - 2026-01-29
6
12
 
7
13
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "8.13.0",
3
+ "version": "9.0.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -79,12 +79,12 @@
79
79
  "test": "bun test"
80
80
  },
81
81
  "dependencies": {
82
- "@oh-my-pi/omp-stats": "8.13.0",
83
- "@oh-my-pi/pi-agent-core": "8.13.0",
84
- "@oh-my-pi/pi-ai": "8.13.0",
85
- "@oh-my-pi/pi-natives": "8.13.0",
86
- "@oh-my-pi/pi-tui": "8.13.0",
87
- "@oh-my-pi/pi-utils": "8.13.0",
82
+ "@oh-my-pi/omp-stats": "9.0.0",
83
+ "@oh-my-pi/pi-agent-core": "9.0.0",
84
+ "@oh-my-pi/pi-ai": "9.0.0",
85
+ "@oh-my-pi/pi-natives": "9.0.0",
86
+ "@oh-my-pi/pi-tui": "9.0.0",
87
+ "@oh-my-pi/pi-utils": "9.0.0",
88
88
  "@openai/agents": "^0.4.4",
89
89
  "@sinclair/typebox": "^0.34.48",
90
90
  "ajv": "^8.17.1",
@@ -460,6 +460,8 @@ export class SettingsManager {
460
460
  private overrides: Settings;
461
461
  private settings!: Settings;
462
462
  private persist: boolean;
463
+ private modifiedFields = new Set<keyof Settings>();
464
+ private modifiedNestedFields = new Map<keyof Settings, Set<string>>();
463
465
 
464
466
  static #lastInstance: SettingsManager | null = null;
465
467
 
@@ -714,6 +716,17 @@ export class SettingsManager {
714
716
  this.rebuildSettings(projectSettings);
715
717
  }
716
718
 
719
+ /** Mark a field as modified during this session */
720
+ private markModified(field: keyof Settings, nestedKey?: string): void {
721
+ this.modifiedFields.add(field);
722
+ if (nestedKey) {
723
+ if (!this.modifiedNestedFields.has(field)) {
724
+ this.modifiedNestedFields.set(field, new Set());
725
+ }
726
+ this.modifiedNestedFields.get(field)!.add(nestedKey);
727
+ }
728
+ }
729
+
717
730
  /**
718
731
  * Persist current global settings to config.yml and rebuild merged settings.
719
732
  * Uses file locking to prevent concurrent write races.
@@ -723,8 +736,32 @@ export class SettingsManager {
723
736
  const configPath = this.configPath;
724
737
  try {
725
738
  await withFileLock(configPath, async () => {
726
- const currentSettings = await SettingsManager.loadFromYaml(configPath);
727
- const mergedSettings = deepMergeSettings(currentSettings, this.globalSettings);
739
+ // Re-read current file to get latest external changes
740
+ const currentFileSettings = await SettingsManager.loadFromYaml(configPath);
741
+
742
+ // Start with file settings as base - preserves external edits
743
+ const mergedSettings: Settings = { ...currentFileSettings };
744
+
745
+ // Only override with in-memory values for fields that were explicitly modified during this session
746
+ for (const field of this.modifiedFields) {
747
+ const value = this.globalSettings[field];
748
+
749
+ // Handle nested objects specially - merge at nested level to preserve unmodified nested keys
750
+ if (this.modifiedNestedFields.has(field) && typeof value === "object" && value !== null) {
751
+ const nestedModified = this.modifiedNestedFields.get(field)!;
752
+ const baseNested = (currentFileSettings[field] as Record<string, unknown>) ?? {};
753
+ const inMemoryNested = value as Record<string, unknown>;
754
+ const mergedNested = { ...baseNested };
755
+ for (const nestedKey of nestedModified) {
756
+ mergedNested[nestedKey] = inMemoryNested[nestedKey];
757
+ }
758
+ (mergedSettings as Record<string, unknown>)[field] = mergedNested;
759
+ } else {
760
+ // For top-level primitives and arrays, use the modified value directly
761
+ (mergedSettings as Record<string, unknown>)[field] = value;
762
+ }
763
+ }
764
+
728
765
  this.globalSettings = mergedSettings;
729
766
  await Bun.write(configPath, YAML.stringify(this.globalSettings, null, 2));
730
767
  });
@@ -743,6 +780,7 @@ export class SettingsManager {
743
780
 
744
781
  async setLastChangelogVersion(version: string): Promise<void> {
745
782
  this.globalSettings.lastChangelogVersion = version;
783
+ this.markModified("lastChangelogVersion");
746
784
  await this.save();
747
785
  }
748
786
 
@@ -761,6 +799,7 @@ export class SettingsManager {
761
799
  this.globalSettings.modelRoles = {};
762
800
  }
763
801
  this.globalSettings.modelRoles[role] = model;
802
+ this.markModified("modelRoles", role);
764
803
 
765
804
  if (this.overrides.modelRoles && this.overrides.modelRoles[role] !== undefined) {
766
805
  this.overrides.modelRoles[role] = model;
@@ -782,6 +821,7 @@ export class SettingsManager {
782
821
 
783
822
  async setSteeringMode(mode: "all" | "one-at-a-time"): Promise<void> {
784
823
  this.globalSettings.steeringMode = mode;
824
+ this.markModified("steeringMode");
785
825
  await this.save();
786
826
  }
787
827
 
@@ -791,6 +831,7 @@ export class SettingsManager {
791
831
 
792
832
  async setFollowUpMode(mode: "all" | "one-at-a-time"): Promise<void> {
793
833
  this.globalSettings.followUpMode = mode;
834
+ this.markModified("followUpMode");
794
835
  await this.save();
795
836
  }
796
837
 
@@ -800,6 +841,7 @@ export class SettingsManager {
800
841
 
801
842
  async setInterruptMode(mode: "immediate" | "wait"): Promise<void> {
802
843
  this.globalSettings.interruptMode = mode;
844
+ this.markModified("interruptMode");
803
845
  await this.save();
804
846
  }
805
847
 
@@ -809,6 +851,7 @@ export class SettingsManager {
809
851
 
810
852
  async setTheme(theme: string): Promise<void> {
811
853
  this.globalSettings.theme = theme;
854
+ this.markModified("theme");
812
855
  await this.save();
813
856
  }
814
857
 
@@ -818,6 +861,7 @@ export class SettingsManager {
818
861
 
819
862
  async setSymbolPreset(preset: SymbolPreset): Promise<void> {
820
863
  this.globalSettings.symbolPreset = preset;
864
+ this.markModified("symbolPreset");
821
865
  await this.save();
822
866
  }
823
867
 
@@ -827,6 +871,7 @@ export class SettingsManager {
827
871
 
828
872
  async setColorBlindMode(enabled: boolean): Promise<void> {
829
873
  this.globalSettings.colorBlindMode = enabled;
874
+ this.markModified("colorBlindMode");
830
875
  await this.save();
831
876
  }
832
877
 
@@ -836,6 +881,7 @@ export class SettingsManager {
836
881
 
837
882
  async setDefaultThinkingLevel(level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"): Promise<void> {
838
883
  this.globalSettings.defaultThinkingLevel = level;
884
+ this.markModified("defaultThinkingLevel");
839
885
  await this.save();
840
886
  }
841
887
 
@@ -848,6 +894,7 @@ export class SettingsManager {
848
894
  this.globalSettings.compaction = {};
849
895
  }
850
896
  this.globalSettings.compaction.enabled = enabled;
897
+ this.markModified("compaction", "enabled");
851
898
  await this.save();
852
899
  }
853
900
 
@@ -892,6 +939,7 @@ export class SettingsManager {
892
939
  this.globalSettings.branchSummary = {};
893
940
  }
894
941
  this.globalSettings.branchSummary.enabled = enabled;
942
+ this.markModified("branchSummary", "enabled");
895
943
  await this.save();
896
944
  }
897
945
 
@@ -911,6 +959,7 @@ export class SettingsManager {
911
959
  this.globalSettings.retry = {};
912
960
  }
913
961
  this.globalSettings.retry.enabled = enabled;
962
+ this.markModified("retry", "enabled");
914
963
  await this.save();
915
964
  }
916
965
 
@@ -942,6 +991,7 @@ export class SettingsManager {
942
991
  this.globalSettings.retry = {};
943
992
  }
944
993
  this.globalSettings.retry.maxRetries = maxRetries;
994
+ this.markModified("retry", "maxRetries");
945
995
  await this.save();
946
996
  }
947
997
 
@@ -954,6 +1004,7 @@ export class SettingsManager {
954
1004
  this.globalSettings.retry = {};
955
1005
  }
956
1006
  this.globalSettings.retry.baseDelayMs = baseDelayMs;
1007
+ this.markModified("retry", "baseDelayMs");
957
1008
  await this.save();
958
1009
  }
959
1010
 
@@ -973,6 +1024,7 @@ export class SettingsManager {
973
1024
  this.globalSettings.todoCompletion = {};
974
1025
  }
975
1026
  this.globalSettings.todoCompletion.enabled = enabled;
1027
+ this.markModified("todoCompletion", "enabled");
976
1028
  await this.save();
977
1029
  }
978
1030
 
@@ -985,6 +1037,7 @@ export class SettingsManager {
985
1037
  this.globalSettings.todoCompletion = {};
986
1038
  }
987
1039
  this.globalSettings.todoCompletion.maxReminders = maxReminders;
1040
+ this.markModified("todoCompletion", "maxReminders");
988
1041
  await this.save();
989
1042
  }
990
1043
 
@@ -998,6 +1051,7 @@ export class SettingsManager {
998
1051
 
999
1052
  async setHideThinkingBlock(hide: boolean): Promise<void> {
1000
1053
  this.globalSettings.hideThinkingBlock = hide;
1054
+ this.markModified("hideThinkingBlock");
1001
1055
  await this.save();
1002
1056
  }
1003
1057
 
@@ -1007,6 +1061,7 @@ export class SettingsManager {
1007
1061
 
1008
1062
  async setShellPath(path: string | undefined): Promise<void> {
1009
1063
  this.globalSettings.shellPath = path;
1064
+ this.markModified("shellPath");
1010
1065
  await this.save();
1011
1066
  }
1012
1067
 
@@ -1016,6 +1071,7 @@ export class SettingsManager {
1016
1071
 
1017
1072
  async setCollapseChangelog(collapse: boolean): Promise<void> {
1018
1073
  this.globalSettings.collapseChangelog = collapse;
1074
+ this.markModified("collapseChangelog");
1019
1075
  await this.save();
1020
1076
  }
1021
1077
 
@@ -1028,6 +1084,7 @@ export class SettingsManager {
1028
1084
  this.globalSettings.startup = {};
1029
1085
  }
1030
1086
  this.globalSettings.startup.quiet = quiet;
1087
+ this.markModified("startup", "quiet");
1031
1088
  await this.save();
1032
1089
  }
1033
1090
 
@@ -1037,6 +1094,7 @@ export class SettingsManager {
1037
1094
 
1038
1095
  async setExtensionPaths(paths: string[]): Promise<void> {
1039
1096
  this.globalSettings.extensions = paths;
1097
+ this.markModified("extensions");
1040
1098
  await this.save();
1041
1099
  }
1042
1100
 
@@ -1049,6 +1107,7 @@ export class SettingsManager {
1049
1107
  this.globalSettings.skills = {};
1050
1108
  }
1051
1109
  this.globalSettings.skills.enabled = enabled;
1110
+ this.markModified("skills", "enabled");
1052
1111
  await this.save();
1053
1112
  }
1054
1113
 
@@ -1076,6 +1135,7 @@ export class SettingsManager {
1076
1135
  this.globalSettings.skills = {};
1077
1136
  }
1078
1137
  this.globalSettings.skills.enableSkillCommands = enabled;
1138
+ this.markModified("skills", "enableSkillCommands");
1079
1139
  await this.save();
1080
1140
  }
1081
1141
 
@@ -1095,6 +1155,7 @@ export class SettingsManager {
1095
1155
  this.globalSettings.commands = {};
1096
1156
  }
1097
1157
  this.globalSettings.commands.enableClaudeUser = enabled;
1158
+ this.markModified("commands", "enableClaudeUser");
1098
1159
  await this.save();
1099
1160
  }
1100
1161
 
@@ -1107,6 +1168,7 @@ export class SettingsManager {
1107
1168
  this.globalSettings.commands = {};
1108
1169
  }
1109
1170
  this.globalSettings.commands.enableClaudeProject = enabled;
1171
+ this.markModified("commands", "enableClaudeProject");
1110
1172
  await this.save();
1111
1173
  }
1112
1174
 
@@ -1119,6 +1181,7 @@ export class SettingsManager {
1119
1181
  this.globalSettings.terminal = {};
1120
1182
  }
1121
1183
  this.globalSettings.terminal.showImages = show;
1184
+ this.markModified("terminal", "showImages");
1122
1185
  await this.save();
1123
1186
  }
1124
1187
 
@@ -1131,6 +1194,7 @@ export class SettingsManager {
1131
1194
  this.globalSettings.notifications = {};
1132
1195
  }
1133
1196
  this.globalSettings.notifications.onComplete = method;
1197
+ this.markModified("notifications", "onComplete");
1134
1198
  await this.save();
1135
1199
  }
1136
1200
 
@@ -1146,6 +1210,7 @@ export class SettingsManager {
1146
1210
  this.globalSettings.ask = {};
1147
1211
  }
1148
1212
  this.globalSettings.ask.timeout = seconds;
1213
+ this.markModified("ask", "timeout");
1149
1214
  await this.save();
1150
1215
  }
1151
1216
 
@@ -1158,6 +1223,7 @@ export class SettingsManager {
1158
1223
  this.globalSettings.ask = {};
1159
1224
  }
1160
1225
  this.globalSettings.ask.notification = method;
1226
+ this.markModified("ask", "notification");
1161
1227
  await this.save();
1162
1228
  }
1163
1229
 
@@ -1170,6 +1236,7 @@ export class SettingsManager {
1170
1236
  this.globalSettings.images = {};
1171
1237
  }
1172
1238
  this.globalSettings.images.autoResize = enabled;
1239
+ this.markModified("images", "autoResize");
1173
1240
  await this.save();
1174
1241
  }
1175
1242
 
@@ -1182,6 +1249,7 @@ export class SettingsManager {
1182
1249
  this.globalSettings.images = {};
1183
1250
  }
1184
1251
  this.globalSettings.images.blockImages = blocked;
1252
+ this.markModified("images", "blockImages");
1185
1253
  await this.save();
1186
1254
  }
1187
1255
 
@@ -1205,6 +1273,7 @@ export class SettingsManager {
1205
1273
  this.globalSettings.exa = {};
1206
1274
  }
1207
1275
  this.globalSettings.exa.enabled = enabled;
1276
+ this.markModified("exa", "enabled");
1208
1277
  await this.save();
1209
1278
  }
1210
1279
 
@@ -1213,6 +1282,7 @@ export class SettingsManager {
1213
1282
  this.globalSettings.exa = {};
1214
1283
  }
1215
1284
  this.globalSettings.exa.enableSearch = enabled;
1285
+ this.markModified("exa", "enableSearch");
1216
1286
  await this.save();
1217
1287
  }
1218
1288
 
@@ -1221,6 +1291,7 @@ export class SettingsManager {
1221
1291
  this.globalSettings.exa = {};
1222
1292
  }
1223
1293
  this.globalSettings.exa.enableLinkedin = enabled;
1294
+ this.markModified("exa", "enableLinkedin");
1224
1295
  await this.save();
1225
1296
  }
1226
1297
 
@@ -1229,6 +1300,7 @@ export class SettingsManager {
1229
1300
  this.globalSettings.exa = {};
1230
1301
  }
1231
1302
  this.globalSettings.exa.enableCompany = enabled;
1303
+ this.markModified("exa", "enableCompany");
1232
1304
  await this.save();
1233
1305
  }
1234
1306
 
@@ -1237,6 +1309,7 @@ export class SettingsManager {
1237
1309
  this.globalSettings.exa = {};
1238
1310
  }
1239
1311
  this.globalSettings.exa.enableResearcher = enabled;
1312
+ this.markModified("exa", "enableResearcher");
1240
1313
  await this.save();
1241
1314
  }
1242
1315
 
@@ -1245,6 +1318,7 @@ export class SettingsManager {
1245
1318
  this.globalSettings.exa = {};
1246
1319
  }
1247
1320
  this.globalSettings.exa.enableWebsets = enabled;
1321
+ this.markModified("exa", "enableWebsets");
1248
1322
  await this.save();
1249
1323
  }
1250
1324
 
@@ -1258,6 +1332,7 @@ export class SettingsManager {
1258
1332
  this.globalSettings.providers = {};
1259
1333
  }
1260
1334
  this.globalSettings.providers.webSearch = provider;
1335
+ this.markModified("providers", "webSearch");
1261
1336
  await this.save();
1262
1337
  }
1263
1338
 
@@ -1270,6 +1345,7 @@ export class SettingsManager {
1270
1345
  this.globalSettings.providers = {};
1271
1346
  }
1272
1347
  this.globalSettings.providers.image = provider;
1348
+ this.markModified("providers", "image");
1273
1349
  await this.save();
1274
1350
  }
1275
1351
 
@@ -1290,6 +1366,7 @@ export class SettingsManager {
1290
1366
  this.globalSettings.bashInterceptor = {};
1291
1367
  }
1292
1368
  this.globalSettings.bashInterceptor.enabled = enabled;
1369
+ this.markModified("bashInterceptor", "enabled");
1293
1370
  await this.save();
1294
1371
  }
1295
1372
 
@@ -1298,6 +1375,7 @@ export class SettingsManager {
1298
1375
  this.globalSettings.bashInterceptor = {};
1299
1376
  }
1300
1377
  this.globalSettings.bashInterceptor.simpleLs = enabled;
1378
+ this.markModified("bashInterceptor", "simpleLs");
1301
1379
  await this.save();
1302
1380
  }
1303
1381
 
@@ -1310,6 +1388,7 @@ export class SettingsManager {
1310
1388
  this.globalSettings.python = {};
1311
1389
  }
1312
1390
  this.globalSettings.python.toolMode = mode;
1391
+ this.markModified("python", "toolMode");
1313
1392
  await this.save();
1314
1393
  }
1315
1394
 
@@ -1322,6 +1401,7 @@ export class SettingsManager {
1322
1401
  this.globalSettings.python = {};
1323
1402
  }
1324
1403
  this.globalSettings.python.kernelMode = mode;
1404
+ this.markModified("python", "kernelMode");
1325
1405
  await this.save();
1326
1406
  }
1327
1407
 
@@ -1334,6 +1414,7 @@ export class SettingsManager {
1334
1414
  this.globalSettings.python = {};
1335
1415
  }
1336
1416
  this.globalSettings.python.sharedGateway = enabled;
1417
+ this.markModified("python", "sharedGateway");
1337
1418
  await this.save();
1338
1419
  }
1339
1420
 
@@ -1346,6 +1427,7 @@ export class SettingsManager {
1346
1427
  this.globalSettings.mcp = {};
1347
1428
  }
1348
1429
  this.globalSettings.mcp.enableProjectConfig = enabled;
1430
+ this.markModified("mcp", "enableProjectConfig");
1349
1431
  await this.save();
1350
1432
  }
1351
1433
 
@@ -1358,6 +1440,7 @@ export class SettingsManager {
1358
1440
  this.globalSettings.lsp = {};
1359
1441
  }
1360
1442
  this.globalSettings.lsp.formatOnWrite = enabled;
1443
+ this.markModified("lsp", "formatOnWrite");
1361
1444
  await this.save();
1362
1445
  }
1363
1446
 
@@ -1370,6 +1453,7 @@ export class SettingsManager {
1370
1453
  this.globalSettings.lsp = {};
1371
1454
  }
1372
1455
  this.globalSettings.lsp.diagnosticsOnWrite = enabled;
1456
+ this.markModified("lsp", "diagnosticsOnWrite");
1373
1457
  await this.save();
1374
1458
  }
1375
1459
 
@@ -1382,6 +1466,7 @@ export class SettingsManager {
1382
1466
  this.globalSettings.lsp = {};
1383
1467
  }
1384
1468
  this.globalSettings.lsp.diagnosticsOnEdit = enabled;
1469
+ this.markModified("lsp", "diagnosticsOnEdit");
1385
1470
  await this.save();
1386
1471
  }
1387
1472
 
@@ -1394,6 +1479,7 @@ export class SettingsManager {
1394
1479
  this.globalSettings.edit = {};
1395
1480
  }
1396
1481
  this.globalSettings.edit.fuzzyMatch = enabled;
1482
+ this.markModified("edit", "fuzzyMatch");
1397
1483
  await this.save();
1398
1484
  }
1399
1485
 
@@ -1406,6 +1492,7 @@ export class SettingsManager {
1406
1492
  this.globalSettings.edit = {};
1407
1493
  }
1408
1494
  this.globalSettings.edit.fuzzyThreshold = value;
1495
+ this.markModified("edit", "fuzzyThreshold");
1409
1496
  await this.save();
1410
1497
  }
1411
1498
 
@@ -1418,6 +1505,7 @@ export class SettingsManager {
1418
1505
  this.globalSettings.edit = {};
1419
1506
  }
1420
1507
  this.globalSettings.edit.patchMode = enabled;
1508
+ this.markModified("edit", "patchMode");
1421
1509
  await this.save();
1422
1510
  }
1423
1511
 
@@ -1430,6 +1518,7 @@ export class SettingsManager {
1430
1518
  this.globalSettings.edit = {};
1431
1519
  }
1432
1520
  this.globalSettings.edit.streamingAbort = enabled;
1521
+ this.markModified("edit", "streamingAbort");
1433
1522
  await this.save();
1434
1523
  }
1435
1524
 
@@ -1481,6 +1570,7 @@ export class SettingsManager {
1481
1570
  } else {
1482
1571
  this.globalSettings.edit.modelVariants[pattern] = variant;
1483
1572
  }
1573
+ this.markModified("edit", "modelVariants");
1484
1574
  await this.save();
1485
1575
  }
1486
1576
 
@@ -1490,6 +1580,7 @@ export class SettingsManager {
1490
1580
 
1491
1581
  async setNormativeRewrite(enabled: boolean): Promise<void> {
1492
1582
  this.globalSettings.normativeRewrite = enabled;
1583
+ this.markModified("normativeRewrite");
1493
1584
  await this.save();
1494
1585
  }
1495
1586
 
@@ -1499,6 +1590,7 @@ export class SettingsManager {
1499
1590
 
1500
1591
  async setReadLineNumbers(enabled: boolean): Promise<void> {
1501
1592
  this.globalSettings.readLineNumbers = enabled;
1593
+ this.markModified("readLineNumbers");
1502
1594
  await this.save();
1503
1595
  }
1504
1596
 
@@ -1508,6 +1600,7 @@ export class SettingsManager {
1508
1600
 
1509
1601
  async setDisabledProviders(providerIds: string[]): Promise<void> {
1510
1602
  this.globalSettings.disabledProviders = providerIds;
1603
+ this.markModified("disabledProviders");
1511
1604
  await this.save();
1512
1605
  }
1513
1606
 
@@ -1517,6 +1610,7 @@ export class SettingsManager {
1517
1610
 
1518
1611
  async setDisabledExtensions(extensionIds: string[]): Promise<void> {
1519
1612
  this.globalSettings.disabledExtensions = extensionIds;
1613
+ this.markModified("disabledExtensions");
1520
1614
  await this.save();
1521
1615
  }
1522
1616
 
@@ -1530,6 +1624,7 @@ export class SettingsManager {
1530
1624
  if (index !== -1) {
1531
1625
  disabled.splice(index, 1);
1532
1626
  this.globalSettings.disabledExtensions = disabled;
1627
+ this.markModified("disabledExtensions");
1533
1628
  await this.save();
1534
1629
  }
1535
1630
  }
@@ -1539,6 +1634,7 @@ export class SettingsManager {
1539
1634
  if (!disabled.includes(extensionId)) {
1540
1635
  disabled.push(extensionId);
1541
1636
  this.globalSettings.disabledExtensions = disabled;
1637
+ this.markModified("disabledExtensions");
1542
1638
  await this.save();
1543
1639
  }
1544
1640
  }
@@ -1549,6 +1645,7 @@ export class SettingsManager {
1549
1645
 
1550
1646
  async setTtsrSettings(settings: TtsrSettings): Promise<void> {
1551
1647
  this.globalSettings.ttsr = { ...this.globalSettings.ttsr, ...settings };
1648
+ this.markModified("ttsr");
1552
1649
  await this.save();
1553
1650
  }
1554
1651
 
@@ -1561,6 +1658,7 @@ export class SettingsManager {
1561
1658
  this.globalSettings.ttsr = {};
1562
1659
  }
1563
1660
  this.globalSettings.ttsr.enabled = enabled;
1661
+ this.markModified("ttsr", "enabled");
1564
1662
  await this.save();
1565
1663
  }
1566
1664
 
@@ -1573,6 +1671,7 @@ export class SettingsManager {
1573
1671
  this.globalSettings.ttsr = {};
1574
1672
  }
1575
1673
  this.globalSettings.ttsr.contextMode = mode;
1674
+ this.markModified("ttsr", "contextMode");
1576
1675
  await this.save();
1577
1676
  }
1578
1677
 
@@ -1585,6 +1684,7 @@ export class SettingsManager {
1585
1684
  this.globalSettings.ttsr = {};
1586
1685
  }
1587
1686
  this.globalSettings.ttsr.repeatMode = mode;
1687
+ this.markModified("ttsr", "repeatMode");
1588
1688
  await this.save();
1589
1689
  }
1590
1690
 
@@ -1597,6 +1697,7 @@ export class SettingsManager {
1597
1697
  this.globalSettings.ttsr = {};
1598
1698
  }
1599
1699
  this.globalSettings.ttsr.repeatGap = gap;
1700
+ this.markModified("ttsr", "repeatGap");
1600
1701
  await this.save();
1601
1702
  }
1602
1703
 
@@ -1620,8 +1721,12 @@ export class SettingsManager {
1620
1721
  delete this.globalSettings.statusLine.leftSegments;
1621
1722
  delete this.globalSettings.statusLine.rightSegments;
1622
1723
  delete this.globalSettings.statusLine.segmentOptions;
1724
+ this.markModified("statusLine", "leftSegments");
1725
+ this.markModified("statusLine", "rightSegments");
1726
+ this.markModified("statusLine", "segmentOptions");
1623
1727
  }
1624
1728
  this.globalSettings.statusLine.preset = preset;
1729
+ this.markModified("statusLine", "preset");
1625
1730
  await this.save();
1626
1731
  }
1627
1732
 
@@ -1634,6 +1739,7 @@ export class SettingsManager {
1634
1739
  this.globalSettings.statusLine = {};
1635
1740
  }
1636
1741
  this.globalSettings.statusLine.separator = separator;
1742
+ this.markModified("statusLine", "separator");
1637
1743
  await this.save();
1638
1744
  }
1639
1745
 
@@ -1646,9 +1752,11 @@ export class SettingsManager {
1646
1752
  this.globalSettings.statusLine = {};
1647
1753
  }
1648
1754
  this.globalSettings.statusLine.leftSegments = segments;
1755
+ this.markModified("statusLine", "leftSegments");
1649
1756
  // Setting segments explicitly implies custom preset
1650
1757
  if (this.globalSettings.statusLine.preset !== "custom") {
1651
1758
  this.globalSettings.statusLine.preset = "custom";
1759
+ this.markModified("statusLine", "preset");
1652
1760
  }
1653
1761
  await this.save();
1654
1762
  }
@@ -1662,9 +1770,11 @@ export class SettingsManager {
1662
1770
  this.globalSettings.statusLine = {};
1663
1771
  }
1664
1772
  this.globalSettings.statusLine.rightSegments = segments;
1773
+ this.markModified("statusLine", "rightSegments");
1665
1774
  // Setting segments explicitly implies custom preset
1666
1775
  if (this.globalSettings.statusLine.preset !== "custom") {
1667
1776
  this.globalSettings.statusLine.preset = "custom";
1777
+ this.markModified("statusLine", "preset");
1668
1778
  }
1669
1779
  await this.save();
1670
1780
  }
@@ -1688,6 +1798,7 @@ export class SettingsManager {
1688
1798
  this.globalSettings.statusLine.segmentOptions[segment] = {} as NonNullable<StatusLineSegmentOptions[K]>;
1689
1799
  }
1690
1800
  (this.globalSettings.statusLine.segmentOptions[segment] as Record<string, unknown>)[option as string] = value;
1801
+ this.markModified("statusLine", "segmentOptions");
1691
1802
  await this.save();
1692
1803
  }
1693
1804
 
@@ -1706,6 +1817,7 @@ export class SettingsManager {
1706
1817
  if (Object.keys(segmentOptions).length === 0) {
1707
1818
  delete this.globalSettings.statusLine?.segmentOptions;
1708
1819
  }
1820
+ this.markModified("statusLine", "segmentOptions");
1709
1821
  await this.save();
1710
1822
  }
1711
1823
 
@@ -1718,6 +1830,7 @@ export class SettingsManager {
1718
1830
  this.globalSettings.statusLine = {};
1719
1831
  }
1720
1832
  this.globalSettings.statusLine.showHookStatus = show;
1833
+ this.markModified("statusLine", "showHookStatus");
1721
1834
  await this.save();
1722
1835
  }
1723
1836
 
@@ -1727,6 +1840,7 @@ export class SettingsManager {
1727
1840
 
1728
1841
  async setDoubleEscapeAction(action: "branch" | "tree"): Promise<void> {
1729
1842
  this.globalSettings.doubleEscapeAction = action;
1843
+ this.markModified("doubleEscapeAction");
1730
1844
  await this.save();
1731
1845
  }
1732
1846
 
@@ -1745,6 +1859,7 @@ export class SettingsManager {
1745
1859
 
1746
1860
  async setShowHardwareCursor(show: boolean): Promise<void> {
1747
1861
  this.globalSettings.showHardwareCursor = show;
1862
+ this.markModified("showHardwareCursor");
1748
1863
  await this.save();
1749
1864
  }
1750
1865
 
@@ -1761,6 +1876,7 @@ export class SettingsManager {
1761
1876
  */
1762
1877
  async setEnvironmentVariables(envVars: Record<string, string>): Promise<void> {
1763
1878
  this.globalSettings.env = { ...envVars };
1879
+ this.markModified("env");
1764
1880
  await this.save();
1765
1881
  }
1766
1882
 
@@ -1769,6 +1885,7 @@ export class SettingsManager {
1769
1885
  */
1770
1886
  async clearEnvironmentVariables(): Promise<void> {
1771
1887
  delete this.globalSettings.env;
1888
+ this.markModified("env");
1772
1889
  await this.save();
1773
1890
  }
1774
1891
 
@@ -1780,6 +1897,7 @@ export class SettingsManager {
1780
1897
  this.globalSettings.env = {};
1781
1898
  }
1782
1899
  this.globalSettings.env[key] = value;
1900
+ this.markModified("env", key);
1783
1901
  await this.save();
1784
1902
  }
1785
1903
 
@@ -1789,6 +1907,7 @@ export class SettingsManager {
1789
1907
  async removeEnvironmentVariable(key: string): Promise<void> {
1790
1908
  if (this.globalSettings.env) {
1791
1909
  delete this.globalSettings.env[key];
1910
+ this.markModified("env", key);
1792
1911
  await this.save();
1793
1912
  }
1794
1913
  }
@@ -502,6 +502,19 @@ export class AgentSession {
502
502
  // Track assistant message for auto-compaction (checked on agent_end)
503
503
  if (event.message.role === "assistant") {
504
504
  this._lastAssistantMessage = event.message;
505
+
506
+ // Reset retry counter immediately on successful assistant response
507
+ // This prevents accumulation across multiple LLM calls within a turn
508
+ const assistantMsg = event.message as AssistantMessage;
509
+ if (assistantMsg.stopReason !== "error" && this._retryAttempt > 0) {
510
+ this._emit({
511
+ type: "auto_retry_end",
512
+ success: true,
513
+ attempt: this._retryAttempt,
514
+ });
515
+ this._retryAttempt = 0;
516
+ this._resolveRetry();
517
+ }
505
518
  }
506
519
 
507
520
  if (event.message.role === "toolResult") {
@@ -530,16 +543,6 @@ export class AgentSession {
530
543
  if (this._isRetryableError(msg)) {
531
544
  const didRetry = await this._handleRetryableError(msg);
532
545
  if (didRetry) return; // Retry was initiated, don't proceed to compaction
533
- } else if (this._retryAttempt > 0) {
534
- // Previous retry succeeded - emit success event and reset counter
535
- this._emit({
536
- type: "auto_retry_end",
537
- success: true,
538
- attempt: this._retryAttempt,
539
- });
540
- this._retryAttempt = 0;
541
- // Resolve the retry promise so waitForRetry() completes
542
- this._resolveRetry();
543
546
  }
544
547
 
545
548
  await this._checkCompaction(msg);
@@ -13,6 +13,17 @@ function tryMacOSScreenshotPath(filePath: string): string {
13
13
  return filePath.replace(/ (AM|PM)\./g, `${NARROW_NO_BREAK_SPACE}$1.`);
14
14
  }
15
15
 
16
+ function tryNFDVariant(filePath: string): string {
17
+ // macOS stores filenames in NFD (decomposed) form, try converting user input to NFD
18
+ return filePath.normalize("NFD");
19
+ }
20
+
21
+ function tryCurlyQuoteVariant(filePath: string): string {
22
+ // macOS uses U+2019 (right single quotation mark) in screenshot names like "Capture d'écran"
23
+ // Users typically type U+0027 (straight apostrophe)
24
+ return filePath.replace(/'/g, "\u2019");
25
+ }
26
+
16
27
  function fileExists(filePath: string): boolean {
17
28
  try {
18
29
  fs.accessSync(filePath, fs.constants.F_OK);
@@ -52,9 +63,28 @@ export function resolveReadPath(filePath: string, cwd: string): string {
52
63
  return resolved;
53
64
  }
54
65
 
55
- const macOSVariant = tryMacOSScreenshotPath(resolved);
56
- if (macOSVariant !== resolved && fileExists(macOSVariant)) {
57
- return macOSVariant;
66
+ // Try macOS AM/PM variant (narrow no-break space before AM/PM)
67
+ const amPmVariant = tryMacOSScreenshotPath(resolved);
68
+ if (amPmVariant !== resolved && fileExists(amPmVariant)) {
69
+ return amPmVariant;
70
+ }
71
+
72
+ // Try NFD variant (macOS stores filenames in NFD form)
73
+ const nfdVariant = tryNFDVariant(resolved);
74
+ if (nfdVariant !== resolved && fileExists(nfdVariant)) {
75
+ return nfdVariant;
76
+ }
77
+
78
+ // Try curly quote variant (macOS uses U+2019 in screenshot names)
79
+ const curlyVariant = tryCurlyQuoteVariant(resolved);
80
+ if (curlyVariant !== resolved && fileExists(curlyVariant)) {
81
+ return curlyVariant;
82
+ }
83
+
84
+ // Try combined NFD + curly quote (for French macOS screenshots like "Capture d'écran")
85
+ const nfdCurlyVariant = tryCurlyQuoteVariant(nfdVariant);
86
+ if (nfdCurlyVariant !== resolved && fileExists(nfdCurlyVariant)) {
87
+ return nfdCurlyVariant;
58
88
  }
59
89
 
60
90
  return resolved;