opencode-copilot-account-switcher 0.13.3 → 0.13.5

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 (39) 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-status-fetcher.d.ts +1 -0
  4. package/dist/codex-status-fetcher.js +10 -0
  5. package/dist/codex-store.js +11 -8
  6. package/dist/common-settings-actions.d.ts +15 -0
  7. package/dist/common-settings-actions.js +42 -0
  8. package/dist/common-settings-store.d.ts +20 -0
  9. package/dist/common-settings-store.js +125 -0
  10. package/dist/menu-runtime.d.ts +1 -0
  11. package/dist/plugin-actions.d.ts +9 -0
  12. package/dist/plugin-actions.js +15 -35
  13. package/dist/plugin-hooks.d.ts +5 -0
  14. package/dist/plugin-hooks.js +167 -43
  15. package/dist/plugin.js +54 -33
  16. package/dist/providers/codex-menu-adapter.d.ts +5 -2
  17. package/dist/providers/codex-menu-adapter.js +23 -0
  18. package/dist/providers/copilot-menu-adapter.d.ts +3 -0
  19. package/dist/providers/copilot-menu-adapter.js +5 -0
  20. package/dist/providers/descriptor.d.ts +3 -2
  21. package/dist/providers/descriptor.js +4 -1
  22. package/dist/providers/registry.d.ts +1 -1
  23. package/dist/providers/registry.js +38 -2
  24. package/dist/retry/codex-policy.d.ts +5 -0
  25. package/dist/retry/codex-policy.js +75 -0
  26. package/dist/retry/common-policy.d.ts +37 -0
  27. package/dist/retry/common-policy.js +68 -0
  28. package/dist/retry/copilot-policy.d.ts +1 -10
  29. package/dist/retry/copilot-policy.js +24 -58
  30. package/dist/store-paths.d.ts +6 -0
  31. package/dist/store-paths.js +24 -0
  32. package/dist/store.js +34 -8
  33. package/dist/ui/menu.d.ts +3 -0
  34. package/dist/ui/menu.js +57 -46
  35. package/dist/upstream/codex-loader-adapter.d.ts +69 -0
  36. package/dist/upstream/codex-loader-adapter.js +55 -0
  37. package/dist/upstream/codex-plugin.snapshot.d.ts +13 -0
  38. package/dist/upstream/codex-plugin.snapshot.js +98 -0
  39. 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")
@@ -421,9 +446,17 @@ function pruneTouchWriteCache(input) {
421
446
  }
422
447
  }
423
448
  export function buildPluginHooks(input) {
449
+ const authProvider = input.auth.provider ?? COPILOT_PROVIDER_DESCRIPTOR.providerIDs[0] ?? "github-copilot";
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;
424
455
  const compactionLoopSafetyBypass = createCompactionLoopSafetyBypass();
425
456
  const loadStore = input.loadStore ?? readStoreSafe;
426
457
  const loadStoreSync = input.loadStoreSync ?? readStoreSafeSync;
458
+ const loadCommonSettings = input.loadCommonSettings;
459
+ const loadCommonSettingsSync = input.loadCommonSettingsSync;
427
460
  const persistStore = (store, meta) => {
428
461
  if (input.writeStore)
429
462
  return input.writeStore(store, meta);
@@ -434,9 +467,30 @@ export function buildPluginHooks(input) {
434
467
  const handleCodexStatusCommandImpl = input.handleCodexStatusCommandImpl ?? handleCodexStatusCommand;
435
468
  const handleCompactCommandImpl = input.handleCompactCommandImpl ?? handleCompactCommand;
436
469
  const handleStopToolCommandImpl = input.handleStopToolCommandImpl ?? handleStopToolCommand;
437
- const loadOfficialConfig = input.loadOfficialConfig ?? loadOfficialCopilotConfig;
438
- const loadOfficialChatHeaders = input.loadOfficialChatHeaders ?? loadOfficialCopilotChatHeaders;
439
- 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);
440
494
  const now = input.now ?? (() => Date.now());
441
495
  const random = input.random ?? Math.random;
442
496
  let injectArmed = false;
@@ -462,6 +516,27 @@ export function buildPluginHooks(input) {
462
516
  });
463
517
  });
464
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
+ };
465
540
  const showInjectToast = async (message, variant = "info") => {
466
541
  await showStatusToast({
467
542
  client: input.client,
@@ -566,6 +641,7 @@ export function buildPluginHooks(input) {
566
641
  const finalHeaderCapture = new AsyncLocalStorage();
567
642
  const getScopedAuth = async () => authOverride.getStore() ?? getAuth();
568
643
  const providerConfig = provider;
644
+ const loadOfficialConfig = enableCodexAuthLoader ? loadOfficialConfigForCodex : loadOfficialConfigForCopilot;
569
645
  const config = await loadOfficialConfig({
570
646
  getAuth: getScopedAuth,
571
647
  provider: providerConfig,
@@ -995,35 +1071,67 @@ export function buildPluginHooks(input) {
995
1071
  }
996
1072
  return response;
997
1073
  };
998
- 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)
999
1082
  return {
1000
1083
  ...config,
1001
- fetch: fetchWithModelAccount,
1084
+ fetch: providerFetch,
1002
1085
  };
1003
1086
  return {
1004
1087
  ...config,
1005
- fetch: createRetryFetch(fetchWithModelAccount, {
1088
+ fetch: createRetryFetch(providerFetch, {
1006
1089
  client: input.client,
1007
1090
  directory: input.directory,
1008
1091
  serverUrl: input.serverUrl,
1009
- lastAccountSwitchAt: retryStore.lastAccountSwitchAt,
1092
+ lastAccountSwitchAt: retryStore?.lastAccountSwitchAt,
1010
1093
  notifier: createCopilotRetryNotifier({
1011
1094
  client: input.client,
1012
- lastAccountSwitchAt: retryStore.lastAccountSwitchAt,
1095
+ lastAccountSwitchAt: retryStore?.lastAccountSwitchAt,
1013
1096
  getLastAccountSwitchAt: getLatestLastAccountSwitchAt,
1014
1097
  clearAccountSwitchContext,
1015
1098
  now: input.now,
1016
1099
  }),
1017
- clearAccountSwitchContext: async () => clearAccountSwitchContext(retryStore.lastAccountSwitchAt),
1100
+ clearAccountSwitchContext: async () => clearAccountSwitchContext(retryStore?.lastAccountSwitchAt),
1018
1101
  }),
1019
1102
  };
1020
1103
  };
1021
- const officialChatHeaders = loadOfficialChatHeaders({
1022
- client: input.client,
1023
- directory: input.directory,
1024
- });
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 () => { });
1025
1127
  const chatHeaders = async (hookInput, output) => {
1026
- 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))
1027
1135
  return;
1028
1136
  const headersBeforeOfficial = { ...output.headers };
1029
1137
  await (await officialChatHeaders)(hookInput, output);
@@ -1121,45 +1229,51 @@ export function buildPluginHooks(input) {
1121
1229
  return {
1122
1230
  auth: {
1123
1231
  ...input.auth,
1124
- provider: input.auth.provider ?? COPILOT_PROVIDER_DESCRIPTOR.providerIDs[0] ?? "github-copilot",
1232
+ provider: authProvider,
1125
1233
  methods: input.auth.methods,
1126
- loader,
1234
+ loader: enableCopilotAuthLoader ? loader : (enableCodexAuthLoader ? codexLoader : undefined),
1127
1235
  },
1128
1236
  config: async (config) => {
1129
1237
  if (!config.command)
1130
1238
  config.command = {};
1131
- const store = loadStoreSync();
1239
+ const store = loadMergedStoreSync();
1132
1240
  if (!areExperimentalSlashCommandsEnabled(store)) {
1133
1241
  return;
1134
1242
  }
1135
- config.command["codex-status"] = {
1136
- template: "Show the current Codex status and usage snapshot via the experimental status path.",
1137
- description: "Experimental Codex status command",
1138
- };
1139
- config.command["copilot-status"] = {
1140
- template: "Show the current GitHub Copilot quota status via the experimental workaround path.",
1141
- description: "Experimental Copilot quota status workaround",
1142
- };
1143
- config.command["copilot-compact"] = {
1144
- template: "Summarize the current session via real session compacting flow.",
1145
- description: "Experimental compact command for Copilot sessions",
1146
- };
1147
- config.command["copilot-stop-tool"] = {
1148
- template: "Interrupt the current session tool flow, annotate the interrupted result, and append a synthetic continue.",
1149
- description: "Experimental interrupt-and-annotate recovery with synthetic continue for Copilot sessions",
1150
- };
1151
- config.command["copilot-inject"] = {
1152
- template: "Arm an immediate tool-output inject marker flow that drives model to question.",
1153
- description: "Experimental force-intervene hook for Copilot workflows",
1154
- };
1155
- config.command["copilot-policy-all-models"] = {
1156
- template: "Toggle the current OpenCode instance policy injection scope between Copilot-only and all providers/models.",
1157
- description: "Experimental policy scope toggle for all providers",
1158
- };
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
+ }
1159
1271
  },
1160
1272
  "command.execute.before": async (hookInput) => {
1161
- const store = await loadStore().catch(() => undefined);
1273
+ const store = await loadMergedStore();
1162
1274
  if (hookInput.command === "copilot-inject") {
1275
+ if (!enableCopilotAuthLoader)
1276
+ return;
1163
1277
  if (!areExperimentalSlashCommandsEnabled(store))
1164
1278
  return;
1165
1279
  injectArmed = true;
@@ -1167,6 +1281,8 @@ export function buildPluginHooks(input) {
1167
1281
  throw new InjectCommandHandledError();
1168
1282
  }
1169
1283
  if (hookInput.command === "copilot-policy-all-models") {
1284
+ if (!enableCopilotAuthLoader)
1285
+ return;
1170
1286
  if (!areExperimentalSlashCommandsEnabled(store))
1171
1287
  return;
1172
1288
  const next = getPolicyScope(store) === "all-models" ? "copilot-only" : "all-models";
@@ -1177,6 +1293,8 @@ export function buildPluginHooks(input) {
1177
1293
  throw new PolicyScopeCommandHandledError();
1178
1294
  }
1179
1295
  if (hookInput.command === "copilot-status") {
1296
+ if (!enableCopilotAuthLoader)
1297
+ return;
1180
1298
  if (!areExperimentalSlashCommandsEnabled(store))
1181
1299
  return;
1182
1300
  await handleStatusCommandImpl({
@@ -1187,6 +1305,8 @@ export function buildPluginHooks(input) {
1187
1305
  });
1188
1306
  }
1189
1307
  if (hookInput.command === "codex-status") {
1308
+ if (!enableCodexAuthLoader)
1309
+ return;
1190
1310
  if (!areExperimentalSlashCommandsEnabled(store))
1191
1311
  return;
1192
1312
  await handleCodexStatusCommandImpl({
@@ -1194,6 +1314,8 @@ export function buildPluginHooks(input) {
1194
1314
  });
1195
1315
  }
1196
1316
  if (hookInput.command === "copilot-compact") {
1317
+ if (!enableCopilotAuthLoader)
1318
+ return;
1197
1319
  if (!areExperimentalSlashCommandsEnabled(store))
1198
1320
  return;
1199
1321
  await handleCompactCommandImpl({
@@ -1203,6 +1325,8 @@ export function buildPluginHooks(input) {
1203
1325
  });
1204
1326
  }
1205
1327
  if (hookInput.command === "copilot-stop-tool") {
1328
+ if (!enableCopilotAuthLoader)
1329
+ return;
1206
1330
  if (!areExperimentalSlashCommandsEnabled(store))
1207
1331
  return;
1208
1332
  await handleStopToolCommandImpl({
@@ -1265,7 +1389,7 @@ export function buildPluginHooks(input) {
1265
1389
  }
1266
1390
  },
1267
1391
  "chat.headers": chatHeaders,
1268
- "experimental.chat.system.transform": createLoopSafetySystemTransform(loadStore, compactionLoopSafetyBypass.consume, lookupSessionAncestry, getPolicyScope),
1392
+ "experimental.chat.system.transform": createLoopSafetySystemTransform(loadMergedStore, compactionLoopSafetyBypass.consume, lookupSessionAncestry, getPolicyScope),
1269
1393
  "experimental.session.compacting": compactionLoopSafetyBypass.hook,
1270
1394
  };
1271
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();
@@ -255,6 +265,7 @@ export function createCodexMenuAdapter(inputDeps) {
255
265
  fallback: `openai-${now()}`,
256
266
  }),
257
267
  providerId: "openai",
268
+ workspaceName: oauth.workspaceName,
258
269
  refresh,
259
270
  access,
260
271
  expires: oauth.expires,
@@ -269,6 +280,7 @@ export function createCodexMenuAdapter(inputDeps) {
269
280
  return Object.entries(store.accounts).map(([name, entry], index) => ({
270
281
  id: entry.accountId ?? name,
271
282
  name: entry.email ?? entry.accountId ?? name,
283
+ workspaceName: entry.workspaceName,
272
284
  index,
273
285
  isCurrent: store.active === name,
274
286
  source: entry.source,
@@ -355,6 +367,17 @@ export function createCodexMenuAdapter(inputDeps) {
355
367
  store.refreshMinutes = Math.max(1, Math.min(180, value));
356
368
  return true;
357
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
+ }
358
381
  return false;
359
382
  },
360
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 {};