opencode-copilot-account-switcher 0.13.4 → 0.13.6

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.
Files changed (36) hide show
  1. package/dist/codex-network-retry.d.ts +2 -0
  2. package/dist/codex-network-retry.js +9 -0
  3. package/dist/codex-store.js +11 -8
  4. package/dist/common-settings-actions.d.ts +15 -0
  5. package/dist/common-settings-actions.js +42 -0
  6. package/dist/common-settings-store.d.ts +20 -0
  7. package/dist/common-settings-store.js +122 -0
  8. package/dist/plugin-actions.d.ts +9 -0
  9. package/dist/plugin-actions.js +15 -35
  10. package/dist/plugin-hooks.d.ts +5 -0
  11. package/dist/plugin-hooks.js +165 -43
  12. package/dist/plugin.js +54 -33
  13. package/dist/providers/codex-menu-adapter.d.ts +5 -2
  14. package/dist/providers/codex-menu-adapter.js +21 -0
  15. package/dist/providers/copilot-menu-adapter.d.ts +3 -0
  16. package/dist/providers/copilot-menu-adapter.js +5 -0
  17. package/dist/providers/descriptor.d.ts +3 -2
  18. package/dist/providers/descriptor.js +4 -1
  19. package/dist/providers/registry.d.ts +1 -1
  20. package/dist/providers/registry.js +38 -2
  21. package/dist/retry/codex-policy.d.ts +5 -0
  22. package/dist/retry/codex-policy.js +75 -0
  23. package/dist/retry/common-policy.d.ts +37 -0
  24. package/dist/retry/common-policy.js +68 -0
  25. package/dist/retry/copilot-policy.d.ts +1 -10
  26. package/dist/retry/copilot-policy.js +24 -58
  27. package/dist/store-paths.d.ts +6 -0
  28. package/dist/store-paths.js +24 -0
  29. package/dist/store.js +34 -8
  30. package/dist/ui/menu.d.ts +2 -0
  31. package/dist/ui/menu.js +56 -46
  32. package/dist/upstream/codex-loader-adapter.d.ts +69 -0
  33. package/dist/upstream/codex-loader-adapter.js +55 -0
  34. package/dist/upstream/codex-plugin.snapshot.d.ts +13 -0
  35. package/dist/upstream/codex-plugin.snapshot.js +98 -0
  36. package/package.json +3 -1
@@ -3,12 +3,15 @@ import { AsyncLocalStorage } from "node:async_hooks";
3
3
  import { createHash } from "node:crypto";
4
4
  import { createCompactionLoopSafetyBypass, createLoopSafetySystemTransform, getLoopSafetyProviderScope, } from "./loop-safety-plugin.js";
5
5
  import { COPILOT_PROVIDER_DESCRIPTOR } from "./providers/descriptor.js";
6
+ import { CODEX_PROVIDER_DESCRIPTOR } from "./providers/descriptor.js";
6
7
  import { createCopilotRetryingFetch, cleanupLongIdsForAccountSwitch, detectRateLimitEvidence, INTERNAL_SESSION_CONTEXT_KEY, } from "./copilot-network-retry.js";
8
+ import { createCodexRetryingFetch } from "./codex-network-retry.js";
7
9
  import { createCopilotRetryNotifier } from "./copilot-retry-notifier.js";
8
10
  import { resolveCopilotModelAccounts } from "./model-account-map.js";
9
11
  import { normalizeDomain } from "./copilot-api-helpers.js";
10
12
  import { readStoreSafe, readStoreSafeSync, writeStore } from "./store.js";
11
13
  import { loadOfficialCopilotConfig, loadOfficialCopilotChatHeaders, } from "./upstream/copilot-loader-adapter.js";
14
+ import { loadOfficialCodexConfig, loadOfficialCodexChatHeaders, } from "./upstream/codex-loader-adapter.js";
12
15
  import { createNotifyTool } from "./notify-tool.js";
13
16
  import { createWaitTool } from "./wait-tool.js";
14
17
  import { refreshActiveAccountQuota } from "./active-account-quota.js";
@@ -39,6 +42,9 @@ export class PolicyScopeCommandHandledError extends Error {
39
42
  function isCopilotProviderID(providerID) {
40
43
  return COPILOT_PROVIDER_DESCRIPTOR.providerIDs.includes(providerID);
41
44
  }
45
+ function isCodexProviderID(providerID) {
46
+ return CODEX_PROVIDER_DESCRIPTOR.providerIDs.includes(providerID);
47
+ }
42
48
  function isDebugEnabled() {
43
49
  return process.env.OPENCODE_COPILOT_RETRY_DEBUG === "1";
44
50
  }
@@ -81,6 +87,25 @@ function areExperimentalSlashCommandsEnabled(store) {
81
87
  return false;
82
88
  return true;
83
89
  }
90
+ function mergeStoreWithCommonSettings(store, common) {
91
+ if (!store && !common)
92
+ return undefined;
93
+ return {
94
+ ...(store ?? { accounts: {} }),
95
+ ...(common?.loopSafetyEnabled === true || common?.loopSafetyEnabled === false
96
+ ? { loopSafetyEnabled: common.loopSafetyEnabled }
97
+ : {}),
98
+ ...(common?.loopSafetyProviderScope
99
+ ? { loopSafetyProviderScope: common.loopSafetyProviderScope }
100
+ : {}),
101
+ ...(common?.networkRetryEnabled === true || common?.networkRetryEnabled === false
102
+ ? { networkRetryEnabled: common.networkRetryEnabled }
103
+ : {}),
104
+ ...(common?.experimentalSlashCommandsEnabled === true || common?.experimentalSlashCommandsEnabled === false
105
+ ? { experimentalSlashCommandsEnabled: common.experimentalSlashCommandsEnabled }
106
+ : {}),
107
+ };
108
+ }
84
109
  async function readRequestBody(request, init) {
85
110
  try {
86
111
  if (typeof init?.body === "string")
@@ -422,10 +447,16 @@ function pruneTouchWriteCache(input) {
422
447
  }
423
448
  export function buildPluginHooks(input) {
424
449
  const authProvider = input.auth.provider ?? COPILOT_PROVIDER_DESCRIPTOR.providerIDs[0] ?? "github-copilot";
425
- const enableCopilotAuthLoader = isCopilotProviderID(authProvider);
450
+ const authLoaderMode = input.authLoaderMode
451
+ ?? (isCopilotProviderID(authProvider) ? "copilot" : isCodexProviderID(authProvider) ? "codex" : "none");
452
+ const enableCopilotAuthLoader = authLoaderMode === "copilot";
453
+ const enableCodexAuthLoader = authLoaderMode === "codex";
454
+ const enableModelRouting = input.enableModelRouting ?? enableCopilotAuthLoader;
426
455
  const compactionLoopSafetyBypass = createCompactionLoopSafetyBypass();
427
456
  const loadStore = input.loadStore ?? readStoreSafe;
428
457
  const loadStoreSync = input.loadStoreSync ?? readStoreSafeSync;
458
+ const loadCommonSettings = input.loadCommonSettings;
459
+ const loadCommonSettingsSync = input.loadCommonSettingsSync;
429
460
  const persistStore = (store, meta) => {
430
461
  if (input.writeStore)
431
462
  return input.writeStore(store, meta);
@@ -436,9 +467,30 @@ export function buildPluginHooks(input) {
436
467
  const handleCodexStatusCommandImpl = input.handleCodexStatusCommandImpl ?? handleCodexStatusCommand;
437
468
  const handleCompactCommandImpl = input.handleCompactCommandImpl ?? handleCompactCommand;
438
469
  const handleStopToolCommandImpl = input.handleStopToolCommandImpl ?? handleStopToolCommand;
439
- const loadOfficialConfig = input.loadOfficialConfig ?? loadOfficialCopilotConfig;
440
- const loadOfficialChatHeaders = input.loadOfficialChatHeaders ?? loadOfficialCopilotChatHeaders;
441
- const createRetryFetch = input.createRetryFetch ?? createCopilotRetryingFetch;
470
+ const loadOfficialConfigForCopilot = (args) => {
471
+ if (input.loadOfficialConfig) {
472
+ return input.loadOfficialConfig(args);
473
+ }
474
+ return loadOfficialCopilotConfig(args);
475
+ };
476
+ const loadOfficialConfigForCodex = (args) => {
477
+ if (input.loadOfficialConfig) {
478
+ return input.loadOfficialConfig(args);
479
+ }
480
+ return loadOfficialCodexConfig({
481
+ getAuth: args.getAuth,
482
+ baseFetch: args.baseFetch,
483
+ version: args.version,
484
+ client: input.client,
485
+ });
486
+ };
487
+ const resolveOfficialChatHeaders = enableCopilotAuthLoader
488
+ ? input.loadOfficialChatHeaders ?? loadOfficialCopilotChatHeaders
489
+ : enableCodexAuthLoader
490
+ ? loadOfficialCodexChatHeaders
491
+ : (async () => async () => { });
492
+ const createRetryFetch = input.createRetryFetch
493
+ ?? (enableCodexAuthLoader ? createCodexRetryingFetch : createCopilotRetryingFetch);
442
494
  const now = input.now ?? (() => Date.now());
443
495
  const random = input.random ?? Math.random;
444
496
  let injectArmed = false;
@@ -464,6 +516,27 @@ export function buildPluginHooks(input) {
464
516
  });
465
517
  });
466
518
  const getPolicyScope = (store) => getLoopSafetyProviderScope(store, policyScopeOverride);
519
+ const loadMergedStore = async () => {
520
+ const [store, common] = await Promise.all([
521
+ loadStore().catch(() => undefined),
522
+ loadCommonSettings?.().catch(() => undefined),
523
+ ]);
524
+ return mergeStoreWithCommonSettings(store, common);
525
+ };
526
+ const loadMergedStoreSync = () => mergeStoreWithCommonSettings(loadStoreSync(), loadCommonSettingsSync?.());
527
+ const isNetworkRetryEnabled = async (retryStore) => {
528
+ if (loadCommonSettings) {
529
+ const common = await loadCommonSettings().catch(() => undefined);
530
+ if (common?.networkRetryEnabled === true)
531
+ return true;
532
+ if (common?.networkRetryEnabled === false)
533
+ return false;
534
+ }
535
+ if (retryStore)
536
+ return retryStore.networkRetryEnabled === true;
537
+ const store = readRetryStoreContext(await loadStore().catch(() => undefined));
538
+ return store?.networkRetryEnabled === true;
539
+ };
467
540
  const showInjectToast = async (message, variant = "info") => {
468
541
  await showStatusToast({
469
542
  client: input.client,
@@ -568,6 +641,7 @@ export function buildPluginHooks(input) {
568
641
  const finalHeaderCapture = new AsyncLocalStorage();
569
642
  const getScopedAuth = async () => authOverride.getStore() ?? getAuth();
570
643
  const providerConfig = provider;
644
+ const loadOfficialConfig = enableCodexAuthLoader ? loadOfficialConfigForCodex : loadOfficialConfigForCopilot;
571
645
  const config = await loadOfficialConfig({
572
646
  getAuth: getScopedAuth,
573
647
  provider: providerConfig,
@@ -997,35 +1071,67 @@ export function buildPluginHooks(input) {
997
1071
  }
998
1072
  return response;
999
1073
  };
1000
- if (retryStore?.networkRetryEnabled !== true)
1074
+ const providerFetch = enableModelRouting
1075
+ ? fetchWithModelAccount
1076
+ : async (request, init) => {
1077
+ const outbound = stripInternalSessionHeader(request, init);
1078
+ return config.fetch(outbound.request, outbound.init);
1079
+ };
1080
+ const networkRetryEnabled = await isNetworkRetryEnabled(retryStore);
1081
+ if (networkRetryEnabled !== true)
1001
1082
  return {
1002
1083
  ...config,
1003
- fetch: fetchWithModelAccount,
1084
+ fetch: providerFetch,
1004
1085
  };
1005
1086
  return {
1006
1087
  ...config,
1007
- fetch: createRetryFetch(fetchWithModelAccount, {
1088
+ fetch: createRetryFetch(providerFetch, {
1008
1089
  client: input.client,
1009
1090
  directory: input.directory,
1010
1091
  serverUrl: input.serverUrl,
1011
- lastAccountSwitchAt: retryStore.lastAccountSwitchAt,
1092
+ lastAccountSwitchAt: retryStore?.lastAccountSwitchAt,
1012
1093
  notifier: createCopilotRetryNotifier({
1013
1094
  client: input.client,
1014
- lastAccountSwitchAt: retryStore.lastAccountSwitchAt,
1095
+ lastAccountSwitchAt: retryStore?.lastAccountSwitchAt,
1015
1096
  getLastAccountSwitchAt: getLatestLastAccountSwitchAt,
1016
1097
  clearAccountSwitchContext,
1017
1098
  now: input.now,
1018
1099
  }),
1019
- clearAccountSwitchContext: async () => clearAccountSwitchContext(retryStore.lastAccountSwitchAt),
1100
+ clearAccountSwitchContext: async () => clearAccountSwitchContext(retryStore?.lastAccountSwitchAt),
1020
1101
  }),
1021
1102
  };
1022
1103
  };
1023
- const officialChatHeaders = loadOfficialChatHeaders({
1024
- client: input.client,
1025
- directory: input.directory,
1026
- });
1104
+ const codexLoader = async (getAuth) => {
1105
+ const config = await loadOfficialConfigForCodex({
1106
+ getAuth: getAuth,
1107
+ }).catch(() => undefined);
1108
+ if (!config || typeof config.fetch !== "function")
1109
+ return {};
1110
+ if (await isNetworkRetryEnabled()) {
1111
+ return {
1112
+ ...config,
1113
+ fetch: createRetryFetch(config.fetch),
1114
+ };
1115
+ }
1116
+ return {
1117
+ ...config,
1118
+ fetch: config.fetch,
1119
+ };
1120
+ };
1121
+ const officialChatHeaders = (enableCopilotAuthLoader || enableCodexAuthLoader)
1122
+ ? resolveOfficialChatHeaders({
1123
+ client: input.client,
1124
+ directory: input.directory,
1125
+ })
1126
+ : Promise.resolve(async () => { });
1027
1127
  const chatHeaders = async (hookInput, output) => {
1028
- if (!isCopilotProviderID(hookInput.model.providerID))
1128
+ if (enableCodexAuthLoader) {
1129
+ if (hookInput.model.providerID !== authProvider)
1130
+ return;
1131
+ await (await officialChatHeaders)(hookInput, output);
1132
+ return;
1133
+ }
1134
+ if (!enableCopilotAuthLoader || !isCopilotProviderID(hookInput.model.providerID))
1029
1135
  return;
1030
1136
  const headersBeforeOfficial = { ...output.headers };
1031
1137
  await (await officialChatHeaders)(hookInput, output);
@@ -1125,43 +1231,49 @@ export function buildPluginHooks(input) {
1125
1231
  ...input.auth,
1126
1232
  provider: authProvider,
1127
1233
  methods: input.auth.methods,
1128
- loader: enableCopilotAuthLoader ? loader : undefined,
1234
+ loader: enableCopilotAuthLoader ? loader : (enableCodexAuthLoader ? codexLoader : undefined),
1129
1235
  },
1130
1236
  config: async (config) => {
1131
1237
  if (!config.command)
1132
1238
  config.command = {};
1133
- const store = loadStoreSync();
1239
+ const store = loadMergedStoreSync();
1134
1240
  if (!areExperimentalSlashCommandsEnabled(store)) {
1135
1241
  return;
1136
1242
  }
1137
- config.command["codex-status"] = {
1138
- template: "Show the current Codex status and usage snapshot via the experimental status path.",
1139
- description: "Experimental Codex status command",
1140
- };
1141
- config.command["copilot-status"] = {
1142
- template: "Show the current GitHub Copilot quota status via the experimental workaround path.",
1143
- description: "Experimental Copilot quota status workaround",
1144
- };
1145
- config.command["copilot-compact"] = {
1146
- template: "Summarize the current session via real session compacting flow.",
1147
- description: "Experimental compact command for Copilot sessions",
1148
- };
1149
- config.command["copilot-stop-tool"] = {
1150
- template: "Interrupt the current session tool flow, annotate the interrupted result, and append a synthetic continue.",
1151
- description: "Experimental interrupt-and-annotate recovery with synthetic continue for Copilot sessions",
1152
- };
1153
- config.command["copilot-inject"] = {
1154
- template: "Arm an immediate tool-output inject marker flow that drives model to question.",
1155
- description: "Experimental force-intervene hook for Copilot workflows",
1156
- };
1157
- config.command["copilot-policy-all-models"] = {
1158
- template: "Toggle the current OpenCode instance policy injection scope between Copilot-only and all providers/models.",
1159
- description: "Experimental policy scope toggle for all providers",
1160
- };
1243
+ if (enableCodexAuthLoader) {
1244
+ config.command["codex-status"] = {
1245
+ template: "Show the current Codex status and usage snapshot via the experimental status path.",
1246
+ description: "Experimental Codex status command",
1247
+ };
1248
+ }
1249
+ if (enableCopilotAuthLoader) {
1250
+ config.command["copilot-status"] = {
1251
+ template: "Show the current GitHub Copilot quota status via the experimental workaround path.",
1252
+ description: "Experimental Copilot quota status workaround",
1253
+ };
1254
+ config.command["copilot-compact"] = {
1255
+ template: "Summarize the current session via real session compacting flow.",
1256
+ description: "Experimental compact command for Copilot sessions",
1257
+ };
1258
+ config.command["copilot-stop-tool"] = {
1259
+ template: "Interrupt the current session tool flow, annotate the interrupted result, and append a synthetic continue.",
1260
+ description: "Experimental interrupt-and-annotate recovery with synthetic continue for Copilot sessions",
1261
+ };
1262
+ config.command["copilot-inject"] = {
1263
+ template: "Arm an immediate tool-output inject marker flow that drives model to question.",
1264
+ description: "Experimental force-intervene hook for Copilot workflows",
1265
+ };
1266
+ config.command["copilot-policy-all-models"] = {
1267
+ template: "Toggle the current OpenCode instance policy injection scope between Copilot-only and all providers/models.",
1268
+ description: "Experimental policy scope toggle for all providers",
1269
+ };
1270
+ }
1161
1271
  },
1162
1272
  "command.execute.before": async (hookInput) => {
1163
- const store = await loadStore().catch(() => undefined);
1273
+ const store = await loadMergedStore();
1164
1274
  if (hookInput.command === "copilot-inject") {
1275
+ if (!enableCopilotAuthLoader)
1276
+ return;
1165
1277
  if (!areExperimentalSlashCommandsEnabled(store))
1166
1278
  return;
1167
1279
  injectArmed = true;
@@ -1169,6 +1281,8 @@ export function buildPluginHooks(input) {
1169
1281
  throw new InjectCommandHandledError();
1170
1282
  }
1171
1283
  if (hookInput.command === "copilot-policy-all-models") {
1284
+ if (!enableCopilotAuthLoader)
1285
+ return;
1172
1286
  if (!areExperimentalSlashCommandsEnabled(store))
1173
1287
  return;
1174
1288
  const next = getPolicyScope(store) === "all-models" ? "copilot-only" : "all-models";
@@ -1179,6 +1293,8 @@ export function buildPluginHooks(input) {
1179
1293
  throw new PolicyScopeCommandHandledError();
1180
1294
  }
1181
1295
  if (hookInput.command === "copilot-status") {
1296
+ if (!enableCopilotAuthLoader)
1297
+ return;
1182
1298
  if (!areExperimentalSlashCommandsEnabled(store))
1183
1299
  return;
1184
1300
  await handleStatusCommandImpl({
@@ -1189,6 +1305,8 @@ export function buildPluginHooks(input) {
1189
1305
  });
1190
1306
  }
1191
1307
  if (hookInput.command === "codex-status") {
1308
+ if (!enableCodexAuthLoader)
1309
+ return;
1192
1310
  if (!areExperimentalSlashCommandsEnabled(store))
1193
1311
  return;
1194
1312
  await handleCodexStatusCommandImpl({
@@ -1196,6 +1314,8 @@ export function buildPluginHooks(input) {
1196
1314
  });
1197
1315
  }
1198
1316
  if (hookInput.command === "copilot-compact") {
1317
+ if (!enableCopilotAuthLoader)
1318
+ return;
1199
1319
  if (!areExperimentalSlashCommandsEnabled(store))
1200
1320
  return;
1201
1321
  await handleCompactCommandImpl({
@@ -1205,6 +1325,8 @@ export function buildPluginHooks(input) {
1205
1325
  });
1206
1326
  }
1207
1327
  if (hookInput.command === "copilot-stop-tool") {
1328
+ if (!enableCopilotAuthLoader)
1329
+ return;
1208
1330
  if (!areExperimentalSlashCommandsEnabled(store))
1209
1331
  return;
1210
1332
  await handleStopToolCommandImpl({
@@ -1267,7 +1389,7 @@ export function buildPluginHooks(input) {
1267
1389
  }
1268
1390
  },
1269
1391
  "chat.headers": chatHeaders,
1270
- "experimental.chat.system.transform": createLoopSafetySystemTransform(loadStore, compactionLoopSafetyBypass.consume, lookupSessionAncestry, getPolicyScope),
1392
+ "experimental.chat.system.transform": createLoopSafetySystemTransform(loadMergedStore, compactionLoopSafetyBypass.consume, lookupSessionAncestry, getPolicyScope),
1271
1393
  "experimental.session.compacting": compactionLoopSafetyBypass.hook,
1272
1394
  };
1273
1395
  }
package/dist/plugin.js CHANGED
@@ -3,8 +3,10 @@ import { listAssignableAccountsForModel, listKnownCopilotModels, rewriteModelAcc
3
3
  import { runProviderMenu } from "./menu-runtime.js";
4
4
  import { persistAccountSwitch } from "./plugin-actions.js";
5
5
  import { buildPluginHooks } from "./plugin-hooks.js";
6
+ import { readCommonSettingsStore, readCommonSettingsStoreSync, writeCommonSettingsStore, } from "./common-settings-store.js";
6
7
  import { createCodexMenuAdapter } from "./providers/codex-menu-adapter.js";
7
8
  import { createCopilotMenuAdapter } from "./providers/copilot-menu-adapter.js";
9
+ import { createProviderRegistry } from "./providers/registry.js";
8
10
  import { isTTY } from "./ui/ansi.js";
9
11
  import { showMenu } from "./ui/menu.js";
10
12
  import { select, selectMany } from "./ui/select.js";
@@ -12,6 +14,27 @@ import { readAuth, readStore, writeStore } from "./store.js";
12
14
  function now() {
13
15
  return Date.now();
14
16
  }
17
+ function toSharedRuntimeAction(action) {
18
+ if (action.type === "cancel")
19
+ return { type: "cancel" };
20
+ if (action.type === "add")
21
+ return { type: "add" };
22
+ if (action.type === "remove-all")
23
+ return { type: "remove-all" };
24
+ if (action.type === "switch")
25
+ return { type: "switch", account: action.account };
26
+ if (action.type === "remove")
27
+ return { type: "remove", account: action.account };
28
+ if (action.type === "toggle-loop-safety")
29
+ return { type: "provider", name: "toggle-loop-safety" };
30
+ if (action.type === "toggle-loop-safety-provider-scope")
31
+ return { type: "provider", name: "toggle-loop-safety-provider-scope" };
32
+ if (action.type === "toggle-experimental-slash-commands")
33
+ return { type: "provider", name: "toggle-experimental-slash-commands" };
34
+ if (action.type === "toggle-network-retry")
35
+ return { type: "provider", name: "toggle-network-retry" };
36
+ return undefined;
37
+ }
15
38
  export async function configureDefaultAccountGroup(store, selectors) {
16
39
  const accountEntries = Object.entries(store.accounts);
17
40
  if (accountEntries.length === 0) {
@@ -253,24 +276,28 @@ async function createAccountSwitcherPlugin(input, provider) {
253
276
  logSwitchHint: () => {
254
277
  console.log("Switched account. If a later Copilot session hits input[*].id too long after switching, enable Copilot Network Retry from the menu.");
255
278
  },
279
+ readCommonSettings: readCommonSettingsStore,
280
+ writeCommonSettings: async (settings) => {
281
+ await writeCommonSettingsStore(settings);
282
+ },
256
283
  });
257
284
  const toRuntimeAction = async (accounts, store) => {
285
+ const common = await readCommonSettingsStore().catch(() => undefined);
258
286
  const action = await showMenu(accounts, {
259
287
  provider: "copilot",
260
288
  refresh: { enabled: store.autoRefresh === true, minutes: store.refreshMinutes ?? 15 },
261
289
  lastQuotaRefresh: store.lastQuotaRefresh,
262
290
  modelAccountAssignmentCount: Object.keys(store.modelAccountAssignments ?? {}).length,
263
291
  defaultAccountGroupCount: store.activeAccountNames?.length ?? (store.active ? 1 : 0),
264
- loopSafetyEnabled: store.loopSafetyEnabled === true,
265
- loopSafetyProviderScope: store.loopSafetyProviderScope,
266
- experimentalSlashCommandsEnabled: store.experimentalSlashCommandsEnabled,
267
- networkRetryEnabled: store.networkRetryEnabled === true,
292
+ loopSafetyEnabled: common?.loopSafetyEnabled ?? store.loopSafetyEnabled === true,
293
+ loopSafetyProviderScope: common?.loopSafetyProviderScope ?? store.loopSafetyProviderScope,
294
+ experimentalSlashCommandsEnabled: common?.experimentalSlashCommandsEnabled ?? store.experimentalSlashCommandsEnabled,
295
+ networkRetryEnabled: common?.networkRetryEnabled ?? store.networkRetryEnabled === true,
268
296
  syntheticAgentInitiatorEnabled: store.syntheticAgentInitiatorEnabled === true,
269
297
  });
270
- if (action.type === "cancel")
271
- return { type: "cancel" };
272
- if (action.type === "add")
273
- return { type: "add" };
298
+ const shared = toSharedRuntimeAction(action);
299
+ if (shared)
300
+ return shared;
274
301
  if (action.type === "import")
275
302
  return { type: "provider", name: "import-auth" };
276
303
  if (action.type === "refresh-identity")
@@ -287,20 +314,6 @@ async function createAccountSwitcherPlugin(input, provider) {
287
314
  return { type: "provider", name: "configure-default-group" };
288
315
  if (action.type === "assign-models")
289
316
  return { type: "provider", name: "assign-models" };
290
- if (action.type === "remove-all")
291
- return { type: "remove-all" };
292
- if (action.type === "switch")
293
- return { type: "switch", account: action.account };
294
- if (action.type === "remove")
295
- return { type: "remove", account: action.account };
296
- if (action.type === "toggle-loop-safety")
297
- return { type: "provider", name: "toggle-loop-safety" };
298
- if (action.type === "toggle-loop-safety-provider-scope")
299
- return { type: "provider", name: "toggle-loop-safety-provider-scope" };
300
- if (action.type === "toggle-experimental-slash-commands")
301
- return { type: "provider", name: "toggle-experimental-slash-commands" };
302
- if (action.type === "toggle-network-retry")
303
- return { type: "provider", name: "toggle-network-retry" };
304
317
  if (action.type === "toggle-synthetic-agent-initiator")
305
318
  return { type: "provider", name: "toggle-synthetic-agent-initiator" };
306
319
  return { type: "cancel" };
@@ -318,28 +331,30 @@ async function createAccountSwitcherPlugin(input, provider) {
318
331
  }
319
332
  const adapter = createCodexMenuAdapter({
320
333
  client: codexClient,
334
+ readCommonSettings: readCommonSettingsStore,
335
+ writeCommonSettings: async (settings) => {
336
+ await writeCommonSettingsStore(settings);
337
+ },
321
338
  });
322
339
  const toRuntimeAction = async (accounts, store) => {
340
+ const common = await readCommonSettingsStore().catch(() => undefined);
323
341
  const action = await showMenu(accounts, {
324
342
  provider: "codex",
325
343
  refresh: { enabled: store.autoRefresh === true, minutes: store.refreshMinutes ?? 15 },
344
+ loopSafetyEnabled: common?.loopSafetyEnabled ?? true,
345
+ loopSafetyProviderScope: common?.loopSafetyProviderScope,
346
+ experimentalSlashCommandsEnabled: common?.experimentalSlashCommandsEnabled,
347
+ networkRetryEnabled: common?.networkRetryEnabled === true,
326
348
  });
327
- if (action.type === "cancel")
328
- return { type: "cancel" };
329
- if (action.type === "add")
330
- return { type: "add" };
349
+ const shared = toSharedRuntimeAction(action);
350
+ if (shared)
351
+ return shared;
331
352
  if (action.type === "quota")
332
353
  return { type: "provider", name: "refresh-snapshot" };
333
354
  if (action.type === "toggle-refresh")
334
355
  return { type: "provider", name: "toggle-refresh" };
335
356
  if (action.type === "set-interval")
336
357
  return { type: "provider", name: "set-interval" };
337
- if (action.type === "remove-all")
338
- return { type: "remove-all" };
339
- if (action.type === "switch")
340
- return { type: "switch", account: action.account };
341
- if (action.type === "remove")
342
- return { type: "remove", account: action.account };
343
358
  return { type: "cancel" };
344
359
  };
345
360
  return runProviderMenu({
@@ -348,7 +363,11 @@ async function createAccountSwitcherPlugin(input, provider) {
348
363
  now,
349
364
  });
350
365
  }
351
- return buildPluginHooks({
366
+ const registry = createProviderRegistry({
367
+ buildPluginHooks,
368
+ });
369
+ const assembled = provider === "github-copilot" ? registry.copilot.descriptor : registry.codex.descriptor;
370
+ return assembled.buildPluginHooks({
352
371
  auth: {
353
372
  provider,
354
373
  methods: provider === "github-copilot" ? copilotMethods : codexMethods,
@@ -356,6 +375,8 @@ async function createAccountSwitcherPlugin(input, provider) {
356
375
  client,
357
376
  directory,
358
377
  serverUrl,
378
+ loadCommonSettings: readCommonSettingsStore,
379
+ loadCommonSettingsSync: readCommonSettingsStoreSync,
359
380
  });
360
381
  }
361
382
  export const CopilotAccountSwitcher = async (input) => {
@@ -3,9 +3,10 @@ import { type CodexOAuthAccount } from "../codex-oauth.js";
3
3
  import { type CodexAccountEntry, type CodexStoreFile } from "../codex-store.js";
4
4
  import type { ProviderMenuAdapter } from "../menu-runtime.js";
5
5
  import { type AccountEntry } from "../store.js";
6
+ import { type CommonSettingsStore } from "../common-settings-store.js";
6
7
  type WriteMeta = {
7
- reason: string;
8
- source: string;
8
+ reason?: string;
9
+ source?: string;
9
10
  actionType?: string;
10
11
  };
11
12
  type AuthClient = {
@@ -42,6 +43,8 @@ type AdapterDependencies = {
42
43
  accountId?: string;
43
44
  }) => Promise<CodexStatusFetcherResult>;
44
45
  runCodexOAuth?: () => Promise<CodexOAuthAccount | undefined>;
46
+ readCommonSettings?: () => Promise<CommonSettingsStore>;
47
+ writeCommonSettings?: (settings: CommonSettingsStore, meta?: WriteMeta) => Promise<void>;
45
48
  };
46
49
  export declare function createCodexMenuAdapter(inputDeps: AdapterDependencies): ProviderMenuAdapter<CodexStoreFile, CodexAccountEntry>;
47
50
  export {};
@@ -5,6 +5,8 @@ import { runCodexOAuth } from "../codex-oauth.js";
5
5
  import { getActiveCodexAccount, readCodexStore, writeCodexStore, } from "../codex-store.js";
6
6
  import { recoverInvalidCodexAccount } from "../codex-invalid-account.js";
7
7
  import { readAuth } from "../store.js";
8
+ import { readCommonSettingsStore, writeCommonSettingsStore, } from "../common-settings-store.js";
9
+ import { applyCommonSettingsAction } from "../common-settings-actions.js";
8
10
  function pickName(input) {
9
11
  const accountId = input.accountId?.trim();
10
12
  if (accountId)
@@ -72,6 +74,14 @@ export function createCodexMenuAdapter(inputDeps) {
72
74
  const loadAuth = inputDeps.readAuthEntries ?? readAuth;
73
75
  const fetchStatus = inputDeps.fetchStatus ?? ((input) => fetchCodexStatus(input));
74
76
  const authorizeOpenAIOAuth = inputDeps.runCodexOAuth ?? runCodexOAuth;
77
+ const readCommonSettings = inputDeps.readCommonSettings ?? readCommonSettingsStore;
78
+ const writeCommonSettings = async (settings, meta) => {
79
+ if (inputDeps.writeCommonSettings) {
80
+ await inputDeps.writeCommonSettings(settings, meta);
81
+ return;
82
+ }
83
+ await writeCommonSettingsStore(settings);
84
+ };
75
85
  const refreshSnapshots = async (store) => {
76
86
  const names = Object.keys(store.accounts);
77
87
  const pendingRecoveryWarnings = new Map();
@@ -357,6 +367,17 @@ export function createCodexMenuAdapter(inputDeps) {
357
367
  store.refreshMinutes = Math.max(1, Math.min(180, value));
358
368
  return true;
359
369
  }
370
+ if (action.name === "toggle-loop-safety"
371
+ || action.name === "toggle-loop-safety-provider-scope"
372
+ || action.name === "toggle-experimental-slash-commands"
373
+ || action.name === "toggle-network-retry") {
374
+ await applyCommonSettingsAction({
375
+ action: { type: action.name },
376
+ readSettings: readCommonSettings,
377
+ writeSettings: writeCommonSettings,
378
+ });
379
+ return false;
380
+ }
360
381
  return false;
361
382
  },
362
383
  };
@@ -1,4 +1,5 @@
1
1
  import type { ProviderMenuAdapter } from "../menu-runtime.js";
2
+ import { type CommonSettingsStore } from "../common-settings-store.js";
2
3
  import { type AccountEntry, type StoreFile, type StoreWriteDebugMeta } from "../store.js";
3
4
  type AuthClient = {
4
5
  auth: {
@@ -60,6 +61,8 @@ type AdapterDependencies = {
60
61
  now?: () => number;
61
62
  }) => Promise<void>;
62
63
  logSwitchHint?: () => void;
64
+ readCommonSettings?: () => Promise<CommonSettingsStore>;
65
+ writeCommonSettings?: (settings: CommonSettingsStore, meta?: DebugMeta) => Promise<void>;
63
66
  };
64
67
  export declare function createCopilotMenuAdapter(inputDeps: AdapterDependencies): ProviderMenuAdapter<StoreFile, AccountEntry>;
65
68
  export {};
@@ -4,6 +4,7 @@ import { fetchQuota } from "../active-account-quota.js";
4
4
  import { getGitHubToken, normalizeDomain } from "../copilot-api-helpers.js";
5
5
  import { listAssignableAccountsForModel, listKnownCopilotModels, rewriteModelAccountAssignments, } from "../model-account-map.js";
6
6
  import { applyMenuAction, persistAccountSwitch } from "../plugin-actions.js";
7
+ import { readCommonSettingsStore, writeCommonSettingsStore, } from "../common-settings-store.js";
7
8
  import { select, selectMany } from "../ui/select.js";
8
9
  import { authPath, readAuth, readStore } from "../store.js";
9
10
  const CLIENT_ID = "Ov23li8tweQw6odWQebz";
@@ -387,6 +388,8 @@ export function createCopilotMenuAdapter(inputDeps) {
387
388
  const fetchUserFn = inputDeps.fetchUser ?? fetchUserDefault;
388
389
  const fetchModelsFn = inputDeps.fetchModels ?? fetchModelsDefault;
389
390
  const fetchQuotaFn = inputDeps.fetchQuota ?? fetchQuota;
391
+ const readCommonSettings = inputDeps.readCommonSettings ?? readCommonSettingsStore;
392
+ const writeCommonSettings = inputDeps.writeCommonSettings ?? writeCommonSettingsStore;
390
393
  let nextAutoRefreshAt = 0;
391
394
  async function maybeAutoRefresh(store) {
392
395
  if (store.autoRefresh !== true || now() < nextAutoRefreshAt)
@@ -728,6 +731,8 @@ export function createCopilotMenuAdapter(inputDeps) {
728
731
  action: { type: action.name },
729
732
  store,
730
733
  writeStore: persistStore,
734
+ readCommonSettings,
735
+ writeCommonSettings,
731
736
  });
732
737
  return false;
733
738
  }
@@ -22,7 +22,8 @@ export declare const CODEX_PROVIDER_DESCRIPTOR: ProviderDescriptor;
22
22
  export declare function createCopilotProviderDescriptor(input: {
23
23
  buildPluginHooks: BuildPluginHooks;
24
24
  }): AssembledProviderDescriptor;
25
- export declare function createCodexProviderDescriptor(input?: {
25
+ export declare function createCodexProviderDescriptor(input: {
26
+ buildPluginHooks: BuildPluginHooks;
26
27
  enabled?: boolean;
27
- }): Omit<AssembledProviderDescriptor, "buildPluginHooks">;
28
+ }): AssembledProviderDescriptor;
28
29
  export {};
@@ -47,6 +47,8 @@ export const CODEX_PROVIDER_DESCRIPTOR = {
47
47
  ],
48
48
  capabilities: [
49
49
  "auth",
50
+ "chat-headers",
51
+ "network-retry",
50
52
  "slash-commands",
51
53
  ],
52
54
  };
@@ -60,12 +62,13 @@ export function createCopilotProviderDescriptor(input) {
60
62
  enabledByDefault: true,
61
63
  };
62
64
  }
63
- export function createCodexProviderDescriptor(input = {}) {
65
+ export function createCodexProviderDescriptor(input) {
64
66
  return {
65
67
  key: "codex",
66
68
  auth: {
67
69
  provider: "openai",
68
70
  },
71
+ buildPluginHooks: input.buildPluginHooks,
69
72
  enabledByDefault: input.enabled !== false,
70
73
  };
71
74
  }
@@ -12,7 +12,7 @@ export declare function createProviderRegistry(input: {
12
12
  descriptor: import("./descriptor.js").AssembledProviderDescriptor;
13
13
  };
14
14
  codex: {
15
- descriptor: Omit<import("./descriptor.js").AssembledProviderDescriptor, "buildPluginHooks">;
15
+ descriptor: import("./descriptor.js").AssembledProviderDescriptor;
16
16
  };
17
17
  };
18
18
  export {};