synabun 2.0.5 → 2.0.7

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 (233) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +2 -1
  3. package/THIRD-PARTY-LICENSES.md +13 -0
  4. package/docs/whatsapp.md +151 -16
  5. package/hooks/claude-code/shared.mjs +6 -3
  6. package/lib/child-path.js +303 -0
  7. package/lib/claude-hooks.js +2 -1
  8. package/lib/data-home-migration.js +2 -0
  9. package/lib/desktop-app.js +265 -0
  10. package/lib/external-tools.js +7 -3
  11. package/lib/log-redact.js +102 -0
  12. package/lib/packaged-runtime.js +138 -11
  13. package/mcp-server/dist/http.d.ts +3 -1
  14. package/mcp-server/dist/http.d.ts.map +1 -1
  15. package/mcp-server/dist/http.js +15 -4
  16. package/mcp-server/dist/http.js.map +1 -1
  17. package/mcp-server/dist/index.d.ts.map +1 -1
  18. package/mcp-server/dist/index.js +9 -6
  19. package/mcp-server/dist/index.js.map +1 -1
  20. package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
  21. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
  22. package/mcp-server/dist/services/budget-judgments.js +153 -0
  23. package/mcp-server/dist/services/budget-judgments.js.map +1 -0
  24. package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
  25. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  26. package/mcp-server/dist/services/codex-config-heal.js +41 -4
  27. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  28. package/mcp-server/dist/services/identity.d.ts +11 -0
  29. package/mcp-server/dist/services/identity.d.ts.map +1 -1
  30. package/mcp-server/dist/services/identity.js +24 -0
  31. package/mcp-server/dist/services/identity.js.map +1 -1
  32. package/mcp-server/dist/services/neural-interface.d.ts +13 -0
  33. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  34. package/mcp-server/dist/services/neural-interface.js +34 -2
  35. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  36. package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
  37. package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
  38. package/mcp-server/dist/services/typesafe-config.js +4 -1
  39. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  40. package/mcp-server/dist/tools/agents.d.ts +10 -7
  41. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  42. package/mcp-server/dist/tools/agents.js +34 -13
  43. package/mcp-server/dist/tools/agents.js.map +1 -1
  44. package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
  45. package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
  46. package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
  47. package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
  48. package/mcp-server/dist/tui.js +2 -1
  49. package/mcp-server/dist/tui.js.map +1 -1
  50. package/mcp-server/dist/version.d.ts +5 -0
  51. package/mcp-server/dist/version.d.ts.map +1 -0
  52. package/mcp-server/dist/version.js +21 -0
  53. package/mcp-server/dist/version.js.map +1 -0
  54. package/mcp-server/package-lock.json +2 -2
  55. package/mcp-server/package.json +1 -1
  56. package/mcp-server/run.mjs +15 -9
  57. package/mcp-server/src/http.ts +18 -3
  58. package/mcp-server/src/index.ts +9 -6
  59. package/mcp-server/src/services/budget-judgments.ts +183 -0
  60. package/mcp-server/src/services/codex-config-heal.ts +35 -4
  61. package/mcp-server/src/services/identity.ts +27 -0
  62. package/mcp-server/src/services/neural-interface.ts +34 -2
  63. package/mcp-server/src/services/typesafe-config.ts +4 -1
  64. package/mcp-server/src/tools/agents.ts +33 -14
  65. package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
  66. package/mcp-server/src/tui.ts +2 -1
  67. package/mcp-server/src/version.ts +22 -0
  68. package/neural-interface/i18n/en.json +627 -57
  69. package/neural-interface/i18n/pt-BR.json +626 -56
  70. package/neural-interface/lib/account-usage.js +163 -0
  71. package/neural-interface/lib/assistant-api.js +255 -52
  72. package/neural-interface/lib/assistant-brains/claude.js +15 -2
  73. package/neural-interface/lib/assistant-brains/codex.js +3 -0
  74. package/neural-interface/lib/assistant-brains/opencode.js +17 -4
  75. package/neural-interface/lib/assistant-budget-planner.js +390 -0
  76. package/neural-interface/lib/assistant-budget-split.js +408 -0
  77. package/neural-interface/lib/assistant-budget.js +94 -95
  78. package/neural-interface/lib/assistant-config.js +62 -14
  79. package/neural-interface/lib/assistant-dispatch.js +432 -125
  80. package/neural-interface/lib/assistant-jev.js +26 -3
  81. package/neural-interface/lib/assistant-persona.js +68 -11
  82. package/neural-interface/lib/assistant-plan-permissions.js +8 -1
  83. package/neural-interface/lib/assistant-route-accounts.js +14 -0
  84. package/neural-interface/lib/assistant-route-gate.js +19 -3
  85. package/neural-interface/lib/assistant-router.js +373 -42
  86. package/neural-interface/lib/assistant-runtime.js +307 -47
  87. package/neural-interface/lib/assistant-user-writer.js +4 -2
  88. package/neural-interface/lib/augmented-path.js +317 -53
  89. package/neural-interface/lib/claude-accounts.js +342 -31
  90. package/neural-interface/lib/claude-agent-bridge.js +90 -3
  91. package/neural-interface/lib/claude-executable.js +6 -1
  92. package/neural-interface/lib/claude-model-catalog.js +3 -3
  93. package/neural-interface/lib/claude-panel-auth.js +221 -0
  94. package/neural-interface/lib/claude-profile-api-key.js +62 -0
  95. package/neural-interface/lib/cli-install-api.js +137 -0
  96. package/neural-interface/lib/cli-install.js +797 -0
  97. package/neural-interface/lib/cli-path.js +130 -0
  98. package/neural-interface/lib/cli-resolver.js +744 -0
  99. package/neural-interface/lib/cli-update-planner.js +143 -7
  100. package/neural-interface/lib/codex-account-auth.js +178 -0
  101. package/neural-interface/lib/codex-accounts.js +489 -0
  102. package/neural-interface/lib/codex-capabilities.js +2 -1
  103. package/neural-interface/lib/codex-compat-rpc.js +8 -2
  104. package/neural-interface/lib/codex-mcp-approvals.js +5 -1
  105. package/neural-interface/lib/codex-panel-auth.js +62 -0
  106. package/neural-interface/lib/codex-session-settings.js +1 -1
  107. package/neural-interface/lib/codex-settings-schema.js +120 -0
  108. package/neural-interface/lib/desktop/config.js +3 -0
  109. package/neural-interface/lib/git-bash/bzip2.js +339 -0
  110. package/neural-interface/lib/git-bash/claude-settings.js +73 -0
  111. package/neural-interface/lib/git-bash/extract-worker.js +34 -0
  112. package/neural-interface/lib/git-bash/install.js +529 -0
  113. package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
  114. package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
  115. package/neural-interface/lib/git-bash/manager.js +268 -0
  116. package/neural-interface/lib/git-bash/pins.js +206 -0
  117. package/neural-interface/lib/git-bash/pins.json +34 -0
  118. package/neural-interface/lib/git-bash/resolver.js +194 -0
  119. package/neural-interface/lib/git-read.js +34 -2
  120. package/neural-interface/lib/install-details.js +145 -0
  121. package/neural-interface/lib/mcp-installer.js +14 -2
  122. package/neural-interface/lib/native-loop-providers.js +16 -3
  123. package/neural-interface/lib/native-loop-runtime.js +9 -4
  124. package/neural-interface/lib/os-path.js +319 -0
  125. package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
  126. package/neural-interface/lib/provider-panel-terminal-auth.js +95 -0
  127. package/neural-interface/lib/pty-host/core.js +2 -1
  128. package/neural-interface/lib/pty-host/manager.js +27 -2
  129. package/neural-interface/lib/remote-policy.js +20 -11
  130. package/neural-interface/lib/session-title-generator.js +4 -3
  131. package/neural-interface/lib/system-about.js +340 -0
  132. package/neural-interface/lib/system-activity.js +66 -0
  133. package/neural-interface/lib/terminal-env.js +11 -3
  134. package/neural-interface/lib/terminal-path.js +55 -0
  135. package/neural-interface/lib/virtual-socket.js +12 -1
  136. package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
  137. package/neural-interface/lib/whatsapp/bridge.js +78 -34
  138. package/neural-interface/lib/whatsapp/commands.js +13 -14
  139. package/neural-interface/lib/whatsapp/config.js +134 -20
  140. package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
  141. package/neural-interface/lib/whatsapp/host-core.js +168 -35
  142. package/neural-interface/lib/whatsapp/inbound.js +5 -3
  143. package/neural-interface/lib/whatsapp/local-file.js +524 -0
  144. package/neural-interface/lib/whatsapp/manager.js +92 -32
  145. package/neural-interface/lib/whatsapp/paths.js +11 -1
  146. package/neural-interface/lib/whatsapp/protocol.js +65 -5
  147. package/neural-interface/lib/whatsapp/redact.js +63 -0
  148. package/neural-interface/lib/whatsapp/service.js +343 -91
  149. package/neural-interface/lib/whatsapp/transcription.js +463 -0
  150. package/neural-interface/package-lock.json +2 -2
  151. package/neural-interface/package.json +1 -1
  152. package/neural-interface/public/index.html +2 -0
  153. package/neural-interface/public/index2d.html +2 -0
  154. package/neural-interface/public/offline.html +16 -1
  155. package/neural-interface/public/onboarding.html +96 -2
  156. package/neural-interface/public/shared/account-i18n.js +7 -0
  157. package/neural-interface/public/shared/account-menu.js +539 -0
  158. package/neural-interface/public/shared/account-usage.js +19 -0
  159. package/neural-interface/public/shared/account-view.js +335 -0
  160. package/neural-interface/public/shared/agent-runtime-options.js +28 -8
  161. package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
  162. package/neural-interface/public/shared/assistant/asst-api.js +32 -2
  163. package/neural-interface/public/shared/assistant/asst-brain-picker.js +124 -25
  164. package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
  165. package/neural-interface/public/shared/assistant/asst-mascot.js +126 -7
  166. package/neural-interface/public/shared/assistant/asst-menu.js +15 -3
  167. package/neural-interface/public/shared/assistant/asst-panel.js +48 -14
  168. package/neural-interface/public/shared/assistant/asst-render.js +38 -7
  169. package/neural-interface/public/shared/assistant/asst-route.js +72 -14
  170. package/neural-interface/public/shared/assistant/asst-routes-editor.js +45 -6
  171. package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
  172. package/neural-interface/public/shared/assistant/asst-state.js +1 -1
  173. package/neural-interface/public/shared/assistant/asst-styles.js +99 -17
  174. package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
  175. package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
  176. package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
  177. package/neural-interface/public/shared/cdx/cdx-panel.js +48 -107
  178. package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
  179. package/neural-interface/public/shared/cdx/cdx-styles.js +128 -107
  180. package/neural-interface/public/shared/cdx/cdx-tabs.js +261 -664
  181. package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
  182. package/neural-interface/public/shared/claude-account-view.js +3 -0
  183. package/neural-interface/public/shared/cli-install-view.js +392 -0
  184. package/neural-interface/public/shared/cli-status.js +282 -22
  185. package/neural-interface/public/shared/cp/cp-events.js +2 -1
  186. package/neural-interface/public/shared/cp/cp-styles.js +46 -0
  187. package/neural-interface/public/shared/desktop-bridge.js +39 -0
  188. package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
  189. package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
  190. package/neural-interface/public/shared/desktop-titlebar.js +153 -0
  191. package/neural-interface/public/shared/desktop-update-view.js +462 -0
  192. package/neural-interface/public/shared/editor/editor-panel.js +1 -1
  193. package/neural-interface/public/shared/explorer/fx-main.js +1 -1
  194. package/neural-interface/public/shared/git-bash-view.js +145 -0
  195. package/neural-interface/public/shared/html-shell.js +1 -0
  196. package/neural-interface/public/shared/i18n.js +19 -1
  197. package/neural-interface/public/shared/install-details-view.js +86 -0
  198. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +32 -0
  199. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +17 -0
  200. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +118 -0
  201. package/neural-interface/public/shared/pwa-install-page.js +3 -1
  202. package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
  203. package/neural-interface/public/shared/settings/settings-about.js +254 -0
  204. package/neural-interface/public/shared/settings/settings-ia.js +15 -0
  205. package/neural-interface/public/shared/showcase-privacy.js +489 -0
  206. package/neural-interface/public/shared/storage.js +30 -3
  207. package/neural-interface/public/shared/styles.css +278 -31
  208. package/neural-interface/public/shared/synabun-mascot.js +48 -2
  209. package/neural-interface/public/shared/ui-claude-panel.js +212 -213
  210. package/neural-interface/public/shared/ui-cli-banner.js +355 -0
  211. package/neural-interface/public/shared/ui-git-bash.js +124 -0
  212. package/neural-interface/public/shared/ui-judgments.js +10 -1
  213. package/neural-interface/public/shared/ui-loading.js +30 -6
  214. package/neural-interface/public/shared/ui-menubar.js +13 -3
  215. package/neural-interface/public/shared/ui-navbar.js +3 -1
  216. package/neural-interface/public/shared/ui-provider-auth.js +104 -0
  217. package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
  218. package/neural-interface/public/shared/ui-sessions.js +9 -3
  219. package/neural-interface/public/shared/ui-settings.js +55 -3
  220. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
  221. package/neural-interface/public/shared/ui-sync.js +6 -0
  222. package/neural-interface/public/shared/ui-terminal.js +17 -0
  223. package/neural-interface/public/shared/ui-tool-updates.js +5 -0
  224. package/neural-interface/public/shared/ui-update.js +594 -5
  225. package/neural-interface/public/shared/ui-whatsapp.js +21 -6
  226. package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
  227. package/neural-interface/public/shared/worker-end.js +2 -0
  228. package/neural-interface/public/sw.js +1 -1
  229. package/neural-interface/public/variant/2d/main.js +10 -1
  230. package/neural-interface/public/variant/3d/main.js +10 -1
  231. package/neural-interface/server.js +1219 -608
  232. package/package.json +4 -1
  233. package/setup.js +9 -8
@@ -1,8 +1,17 @@
1
+ import { mountCodexSettings } from './cdx-settings.js';
2
+ import { t } from '../i18n.js';
3
+ import { getCliState, isCliInstalling } from '../cli-status.js';
4
+ import { cliInstallText } from '../cli-install-view.js';
1
5
  // ═══════════════════════════════════════════
2
6
  // SynaBun — Codex Panel: State, Tabs & Sessions
3
7
  // ═══════════════════════════════════════════
4
8
 
5
9
  import { storage } from '../storage.js';
10
+ import { providerAuthSnapshot } from '../ui-provider-auth.js';
11
+ import { mountProviderTerminalAuth, providerApiKeyRequest, startProviderTerminalLogin } from '../ui-provider-terminal-auth.js';
12
+ import { accountName, accountSummary, accountSwitchPlan, accountText, serverAccountId } from '../account-view.js';
13
+ import { mountAccountMenu } from '../account-menu.js';
14
+ import '../account-i18n.js';
6
15
  import { restoreMcpAppCards, teardownMcpApps, retireMcpAppNode, stripMcpAppRuntime } from './cdx-mcp-app-host.js';
7
16
  import { renderTurnDiff, completeTurnOutputs } from './cdx-render.js';
8
17
  import { hydrateOutputControls } from './cdx-output.js';
@@ -29,14 +38,14 @@ import { fileLinkText } from '../ui-file-links.js';
29
38
  import { WORKER_END_CLASS, backToAssistant, buildWorkerEndCard, endWorkerRunOnClose, forgetWorkerEnd } from '../ui-worker-end.js';
30
39
  import {
31
40
  CODEX_COMMANDS, CODEX_COMMAND_BY_NAME, parseCodexCommand, codexCommandHints,
32
- capabilityAvailability, latestCompletedCodexText, configValue, configRestriction,
33
- configChanges, SESSION_CONFIG_FIELDS, sessionSettings, flattenNativeCatalog, modelServiceTiers, fastServiceTier,
41
+ capabilityAvailability, latestCompletedCodexText, configValue,
42
+ SESSION_CONFIG_FIELDS, flattenNativeCatalog, fastServiceTier,
34
43
  } from './cdx-commands.js';
35
44
  import {
36
45
  knownCodexCapabilities, storeCodexCapabilities, dropCodexCapabilities, invalidateCodexCapabilities,
37
46
  currentCodexCapabilities, requestWithCodexCapability,
38
47
  } from './cdx-capabilities.js';
39
- import { openCodexCatalog, openCodexReview, openCodexGoal, openCodexFeedback, openCodexDiff, refreshCodexDiff, refreshCodexAttachments, mountCodexAccountSecurity, updateCodexGateway } from './cdx-features.js';
48
+ import { openCodexCatalog, openCodexReview, openCodexGoal, openCodexFeedback, openCodexDiff, refreshCodexDiff, refreshCodexAttachments, updateCodexGateway } from './cdx-features.js';
40
49
  export { refreshCodexDiff } from './cdx-features.js';
41
50
  import {
42
51
  STOR, EFFORT_LEVELS, MAX_THREAD_SNAPSHOTS, MAX_THREAD_SNAPSHOT_CHARS,
@@ -412,6 +421,7 @@ function buildWsCallbacks(tab, options = {}) {
412
421
  }
413
422
  restoreMcpAppCards(_messagesEl, { tab: _boundTab, items: _items, request: requestSocketPayloadForTab, draft: setMcpAppDraft });
414
423
  if (_boundTab) _boundTab.bootstrapped = true;
424
+ ensureCodexTerminalAuth(tab);
415
425
  if (msg.mcpProfile && _boundTab) {
416
426
  _boundTab.mcpProfile = msg.mcpProfile;
417
427
  if (activeTab() === _boundTab) _onActiveTabChanged?.(_boundTab);
@@ -642,7 +652,8 @@ function buildWsCallbacks(tab, options = {}) {
642
652
  return;
643
653
  }
644
654
  if (msg.type === 'codex_config_repaired') {
645
- appendSystem(msg.message || 'Codex configuration repaired.', 'muted');
655
+ clearCodexConfigWarnings();
656
+ appendSystem(t('codexSettings.configRepaired'), 'muted');
646
657
  return;
647
658
  }
648
659
  resolveGenericRequest(msg);
@@ -1253,6 +1264,7 @@ function writeThreadSnapshot(tab, { force = false } = {}) {
1253
1264
  const snapshotNodes = [...messagesEl.children].filter((node) => (
1254
1265
  !node.classList?.contains('cxp-empty') && !node.classList?.contains('cxp-thinking')
1255
1266
  && !node.classList?.contains(WORKER_END_CLASS)
1267
+ && !node.hasAttribute('data-provider-auth-card')
1256
1268
  ));
1257
1269
  const ownsEveryNode = codexTranscriptNodesHaveOwnership(snapshotNodes, { threadId, accountId });
1258
1270
  if (!ownsEveryNode) {
@@ -1265,7 +1277,7 @@ function writeThreadSnapshot(tab, { force = false } = {}) {
1265
1277
  stripMcpAppRuntime(snapshotEl);
1266
1278
  // A worker's end card is drawn again from its run, never restored from markup.
1267
1279
  snapshotEl.querySelectorAll(`.${WORKER_END_CLASS}`).forEach((node) => node.remove());
1268
- const html = snapshotEl.innerHTML || '';
1280
+ const html = providerAuthSnapshot(snapshotEl);
1269
1281
  if (!html.trim()) return;
1270
1282
  const ownedItemNodes = snapshotNodes.filter((node) => node.dataset?.itemId);
1271
1283
  const itemCount = normalizeSnapshotItemCount(tab?.transcriptItemCount)
@@ -2044,6 +2056,8 @@ export function syncLegacyState() {
2044
2056
  else storage.removeItem(STOR.project);
2045
2057
  if (tab.threadId) storage.setItem(STOR.threadId, tab.threadId);
2046
2058
  else storage.removeItem(STOR.threadId);
2059
+ if (tab.threadId) storage.setItem(STOR.threadAccount, tab.accountId || 'default');
2060
+ else storage.removeItem(STOR.threadAccount);
2047
2061
  if (tab.threadId && tab.sessionLabel) storage.setItem(STOR.title, tab.sessionLabel);
2048
2062
  else storage.removeItem(STOR.title);
2049
2063
  }
@@ -2585,6 +2599,8 @@ function disposeDiscardedTab(tab) {
2585
2599
  if (!tab) return;
2586
2600
  flushThreadSnapshotSave(tab);
2587
2601
  disconnectTab(tab);
2602
+ tab.authView?.dispose();
2603
+ tab.terminalAuthView?.dispose();
2588
2604
  tab.closed = true;
2589
2605
  if (tab.snapshotTimer) {
2590
2606
  clearTimeout(tab.snapshotTimer);
@@ -3121,10 +3137,14 @@ export function restoreTabs() {
3121
3137
  }
3122
3138
  } catch {}
3123
3139
 
3140
+ // A window with no tabs of its own reopens the last thread, under the account it belongs to: its rollout
3141
+ // lives in that account's CODEX_HOME, never Default's.
3142
+ const lastThreadId = storage.getItem(STOR.threadId) || null;
3124
3143
  createTab({
3125
3144
  project: storage.getItem(STOR.project) || '',
3126
- threadId: storage.getItem(STOR.threadId) || null,
3145
+ threadId: lastThreadId,
3127
3146
  title: storage.getItem(STOR.title) || '',
3147
+ ...(lastThreadId ? { accountId: storage.getItem(STOR.threadAccount) || 'default' } : {}),
3128
3148
  }, { autoSwitch: false });
3129
3149
  _activeTabIdx = 0;
3130
3150
  updateActiveTabView();
@@ -3886,12 +3906,24 @@ export function cxpSessRebuildList(menu, listEl, tab) {
3886
3906
  }
3887
3907
 
3888
3908
  // ═══════════════════════════════════════════
3889
- // Multi-account (per-tab ChatGPT account)
3909
+ // Codex accounts (per tab): the header account menu
3890
3910
  // ═══════════════════════════════════════════
3911
+ // Every account and its state come from the server (lib/codex-accounts.js
3912
+ // describe(): its mode, whether it is signed in, its identity: an email or a
3913
+ // masked key, never a key); account-view.js decides the words, account-menu.js
3914
+ // draws the menu (the same component as the Claude panel's). /account, /login,
3915
+ // the sign-in screen's "Other accounts…" and the Context settings popover's
3916
+ // Account row open it. A conversation belongs to the account it started under
3917
+ // (its thread lives in that account's CODEX_HOME): a tab with one opens another
3918
+ // account in a new tab; before its first message the tab itself switches.
3919
+
3920
+ let _codexAccountsError = '';
3921
+ let _codexAccountsLoad = null;
3922
+ let _accountMenu = null;
3891
3923
 
3892
3924
  function accountLabelFor(id) {
3893
3925
  const acct = _codexAccounts.find((a) => a.id === id);
3894
- return acct ? (acct.label || acct.email || (acct.isDefault ? 'Default' : 'account')) : (id === 'default' ? 'Default' : 'account');
3926
+ return acct ? accountName(acct, 'codex') : (id === 'default' ? accountText('defaultName', {}, 'codex') : 'account');
3895
3927
  }
3896
3928
 
3897
3929
  // The tab's account as the Context settings popover names it: the label, and
@@ -3900,29 +3932,35 @@ function accountView(tab) {
3900
3932
  const acctId = tab?.accountId || _accountId || 'default';
3901
3933
  const acct = _codexAccounts.find((a) => a.id === acctId);
3902
3934
  return {
3903
- label: acct
3904
- ? (acct.label || acct.email || (acct.isDefault ? 'Default' : 'Account'))
3905
- : (acctId === 'default' ? 'Default' : 'Account'),
3906
- tip: acct?.email
3907
- ? `Account: ${acct.email}${acct.planType ? ` (${acct.planType})` : ''} — click to switch`
3908
- : 'Switch ChatGPT account for this tab',
3935
+ label: acct ? accountName(acct, 'codex') : accountLabelFor(acctId),
3936
+ tip: acct ? accountSummary(acct, 'codex') : accountText('menuTitle', {}, 'codex'),
3909
3937
  };
3910
3938
  }
3911
3939
 
3912
- // The account is a row of the Context settings popover.
3940
+ // The account is a row of the Context settings popover and the header menu's trigger.
3913
3941
  export function syncAccountChip() {
3914
3942
  syncContextMenu();
3915
- }
3916
-
3917
- export function refreshCodexAccounts() {
3918
- requestSocketPayload('account_list', {})
3919
- .then((msg) => {
3920
- if (Array.isArray(msg?.accounts)) {
3921
- _codexAccounts = msg.accounts;
3922
- syncAccountChip();
3923
- }
3924
- })
3925
- .catch(() => {});
3943
+ _accountMenu?.render();
3944
+ }
3945
+
3946
+ /** Read every account with its state (GET /api/codex/accounts); `refresh` asks Codex about each first. */
3947
+ export function refreshCodexAccounts({ refresh = false } = {}) {
3948
+ if (_codexAccountsLoad && !refresh) return _codexAccountsLoad;
3949
+ const load = fetch(`/api/codex/accounts${refresh ? '?refresh=true' : ''}`, { cache: 'no-store' })
3950
+ .then((response) => response.json().catch(() => ({})).then((data) => {
3951
+ if (!response.ok || !Array.isArray(data?.accounts)) throw new Error(data?.error || accountText('readFailed', {}, 'codex'));
3952
+ _codexAccounts = data.accounts;
3953
+ _codexAccountsError = '';
3954
+ return _codexAccounts;
3955
+ }))
3956
+ .catch((error) => { _codexAccountsError = accountText('readFailed', {}, 'codex'); throw error; })
3957
+ .finally(() => {
3958
+ if (_codexAccountsLoad === load) _codexAccountsLoad = null;
3959
+ syncAccountChip();
3960
+ });
3961
+ _codexAccountsLoad = load;
3962
+ load.catch(() => {});
3963
+ return load;
3926
3964
  }
3927
3965
 
3928
3966
  function resetViewForAccountChange(label) {
@@ -3948,29 +3986,33 @@ function resetViewForAccountChange(label) {
3948
3986
  }
3949
3987
  setSessionLabel('New session', { persist: false });
3950
3988
  clearTranscript(freshThreadText());
3989
+ if (_boundTab) ensureCodexTerminalAuth(_boundTab);
3951
3990
  if (label) setStatus(label, 'ready');
3952
3991
  }
3953
3992
 
3954
- function applyAccountReadToCache(accountId, readResult) {
3955
- const info = readResult?.account || readResult || {};
3956
- const acct = _codexAccounts.find((a) => a.id === accountId);
3957
- if (acct && info.email) {
3958
- acct.email = info.email;
3959
- acct.planType = info.planType || acct.planType;
3960
- if (!acct.label || acct.label === 'New account') acct.label = info.email;
3961
- }
3993
+ // What the server says an account holds, for the sign-in screen: { loggedIn } (true / false / unknown).
3994
+ function signInStateOf(row) {
3995
+ return row && typeof row.signedIn === 'boolean' ? { loggedIn: row.signedIn } : null;
3962
3996
  }
3963
3997
 
3964
3998
  function handleAccountMessage(msg) {
3999
+ const ownsAccount = (!msg.accountId || msg.accountId === _boundTab?.accountId)
4000
+ && (!msg.state?.accountId || msg.state.accountId === _boundTab?.accountId);
3965
4001
  if (Array.isArray(msg.accounts)) _codexAccounts = msg.accounts;
3966
- if (msg.type === 'account_info' && msg.account) {
3967
- applyAccountReadToCache(msg.accountId || _accountId || 'default', msg.account);
3968
- if (!msg.error && (!msg.accountId || msg.accountId === _accountId)) refreshModelCatalog(_boundTab);
3969
- }
4002
+ // The server's reading of the account (accountState / account_state) is what every surface shows.
4003
+ const state = msg.type === 'account_state' ? msg.account : msg.accountState;
4004
+ if (state?.id) {
4005
+ const index = _codexAccounts.findIndex((a) => a.id === state.id);
4006
+ if (index >= 0) _codexAccounts[index] = state; else _codexAccounts.push(state);
4007
+ }
4008
+ if (ownsAccount && !msg.error) {
4009
+ const signIn = signInStateOf(state);
4010
+ if (signIn) _boundTab?.terminalAuthView?.update(signIn);
4011
+ if (msg.type === 'account_logged_out' && !state) _boundTab?.terminalAuthView?.update({ loggedIn: false });
4012
+ }
4013
+ if (msg.type === 'account_info' && !msg.error && (!msg.accountId || msg.accountId === _accountId)) refreshModelCatalog(_boundTab);
3970
4014
  if (msg.type === 'account_logged_out' && !msg.error) refreshModelCatalog(_boundTab);
3971
- if (msg.type === 'account_switched' && msg.account) {
3972
- applyAccountReadToCache(msg.accountId || _accountId || 'default', msg.account);
3973
- }
4015
+ if (msg.type === 'account_state' && ownsAccount) { invalidateCodexFeatureCaches(_boundTab); refreshModelCatalog(_boundTab); }
3974
4016
  if (msg.type === 'account_switched' && msg.accountId) {
3975
4017
  _accountId = msg.accountId;
3976
4018
  if (_boundTab) _boundTab.accountId = msg.accountId;
@@ -3978,139 +4020,118 @@ function handleAccountMessage(msg) {
3978
4020
  resetViewForAccountChange(`Switched to ${accountLabelFor(msg.accountId)}`);
3979
4021
  refreshModelCatalog(_boundTab);
3980
4022
  }
3981
- if (msg.type === 'account_add_started') {
3982
- if (msg.accountId) {
3983
- _accountId = msg.accountId;
3984
- if (_boundTab) _boundTab.accountId = msg.accountId;
3985
- saveTabs();
3986
- }
4023
+ if (msg.type === 'account_add_started' && msg.accountId) {
4024
+ _accountId = msg.accountId;
4025
+ if (_boundTab) _boundTab.accountId = msg.accountId;
4026
+ saveTabs();
3987
4027
  resetViewForAccountChange('');
3988
- appendSystem('Opening ChatGPT login in your browser… complete sign-in, then return here. The account name updates automatically once you finish.', 'working');
3989
4028
  }
3990
4029
  syncAccountChip();
3991
- const menu = panelEl('#cxp-account-menu');
3992
- if (menu?.classList.contains('open')) renderAccountMenu();
3993
4030
  }
3994
4031
 
3995
- export function toggleAccountMenu() {
3996
- const menu = panelEl('#cxp-account-menu');
3997
- if (!menu) return;
3998
- const isOpen = menu.classList.toggle('open');
3999
- if (isOpen) { renderAccountMenu(); refreshCodexAccounts(); }
4032
+ // The header account menu (mounted by cdx-panel.js into the header's actions).
4033
+ export function mountCodexAccountMenu(actionsEl, panelEl) {
4034
+ if (_accountMenu || !actionsEl) return _accountMenu;
4035
+ const path = (id) => `/api/codex/accounts/${encodeURIComponent(id)}`;
4036
+ _accountMenu = mountAccountMenu({
4037
+ provider: 'codex', container: actionsEl, before: actionsEl.firstChild, layer: () => panelEl,
4038
+ read: () => {
4039
+ const tab = activeTab();
4040
+ return {
4041
+ rows: _codexAccounts, currentId: tab?.accountId || 'default', hasConversation: !!tab?.threadId,
4042
+ available: !!tab && !tab.automationRunId, busy: !!(tab?.running || tab?.automationActive),
4043
+ loading: !_codexAccounts.length && !_codexAccountsError, error: _codexAccountsError,
4044
+ };
4045
+ },
4046
+ actions: {
4047
+ load: ({ refresh }) => refreshCodexAccounts({ refresh }),
4048
+ pick: (id) => { const tab = activeTab(); if (tab) pickCodexAccount(tab, id); },
4049
+ add: async () => (await codexAccountRequest('/api/codex/accounts', { method: 'POST', body: '{}' })).account,
4050
+ signIn: (id) => startProviderTerminalLogin('codex', id, `Codex sign-in · ${accountLabelFor(id)}`),
4051
+ saveKey: (id, key) => providerApiKeyRequest('codex', id, key),
4052
+ setMode: (id, mode) => codexAccountRequest(path(id), { method: 'PATCH', body: JSON.stringify({ mode }) }),
4053
+ signOut: (id, credential) => codexAccountRequest(`${path(id)}/auth/logout`, { method: 'POST', body: JSON.stringify({ credential }) }),
4054
+ rename: (id, label) => codexAccountRequest(path(id), { method: 'PATCH', body: JSON.stringify({ label }) }),
4055
+ remove: (id) => removeCodexAccount(id),
4056
+ },
4057
+ });
4058
+ refreshCodexAccounts().catch(() => {});
4059
+ return _accountMenu;
4000
4060
  }
4001
4061
 
4002
- function renderAccountMenu() {
4003
- const menu = panelEl('#cxp-account-menu');
4004
- if (!menu) return;
4005
- const tab = activeTab();
4006
- const activeAcctId = tab?.accountId || _accountId || 'default';
4007
- menu.innerHTML = '';
4008
-
4009
- const list = document.createElement('div');
4010
- list.className = 'cxp-account-list';
4011
- if (!_codexAccounts.length) {
4012
- const empty = document.createElement('div');
4013
- empty.className = 'cxp-account-empty';
4014
- empty.textContent = 'Loading accounts…';
4015
- list.appendChild(empty);
4016
- }
4017
- for (const acct of _codexAccounts) {
4018
- const row = document.createElement('div');
4019
- row.className = 'cxp-account-row' + (acct.id === activeAcctId ? ' active' : '');
4020
- const main = document.createElement('button');
4021
- main.className = 'cxp-account-row-main';
4022
- main.type = 'button';
4023
- const sub = acct.email && acct.email !== acct.label
4024
- ? `<span class="cxp-account-row-sub">${esc(acct.email)}${acct.planType ? ` · ${esc(acct.planType)}` : ''}</span>`
4025
- : (acct.planType ? `<span class="cxp-account-row-sub">${esc(acct.planType)}</span>` : '');
4026
- main.innerHTML = `
4027
- <span class="cxp-account-check">${acct.id === activeAcctId ? '&#x2713;' : ''}</span>
4028
- <span class="cxp-account-row-text">
4029
- <span class="cxp-account-row-label">${esc(acct.label || acct.email || (acct.isDefault ? 'Default' : 'Account'))}</span>
4030
- ${sub}
4031
- </span>`;
4032
- main.addEventListener('click', () => { menu.classList.remove('open'); switchToAccount(acct.id); });
4033
- row.appendChild(main);
4034
- if (!acct.isDefault) {
4035
- const removeBtn = document.createElement('button');
4036
- removeBtn.className = 'cxp-account-row-remove';
4037
- removeBtn.type = 'button';
4038
- removeBtn.title = 'Remove account';
4039
- removeBtn.innerHTML = '&#x2715;';
4040
- removeBtn.addEventListener('click', (e) => { e.stopPropagation(); removeCodexAccount(acct.id); });
4041
- row.appendChild(removeBtn);
4042
- }
4043
- list.appendChild(row);
4044
- }
4045
- menu.appendChild(list);
4046
-
4047
- const addBtn = document.createElement('button');
4048
- addBtn.className = 'cxp-account-add';
4049
- addBtn.type = 'button';
4050
- addBtn.textContent = '+ Add ChatGPT account…';
4051
- addBtn.addEventListener('click', () => { menu.classList.remove('open'); addCodexAccount(); });
4052
- menu.appendChild(addBtn);
4062
+ /** Open the header account menu (`manage`: one account's management). */
4063
+ export function openAccountMenu({ manage = '' } = {}) {
4064
+ if (!_accountMenu) return;
4065
+ _accountMenu.open(manage ? { manage } : {});
4066
+ }
4067
+ // The Context settings popover's Account row and older callers.
4068
+ export function toggleAccountMenu() {
4069
+ if (_accountMenu?.isOpen()) _accountMenu.close({ restore: true });
4070
+ else openAccountMenu();
4053
4071
  }
4054
4072
 
4055
- async function switchToAccount(accountId) {
4056
- const tab = activeTab();
4057
- if (!tab) return;
4058
- if ((tab.accountId || 'default') === accountId) return;
4059
- if (tab.running) { appendSystem('Finish or stop the current turn before switching accounts.', 'error'); return; }
4060
- try {
4061
- setStatus('Switching account…', 'working');
4062
- await requestSocketPayloadForTab(tab, 'account_switch', { accountId });
4063
- // account_switched push (handled in handleAccountMessage) updates the view.
4064
- } catch (err) {
4065
- if (!tab.closed) withTab(tab, () => {
4066
- appendSystem(err.message || 'Could not switch account', 'error');
4067
- setStatus(err.message || 'Switch failed', 'error');
4068
- });
4069
- }
4073
+ async function codexAccountRequest(url, options = {}) {
4074
+ const response = await fetch(url, { ...options, cache: 'no-store', headers: { 'Content-Type': 'application/json' } });
4075
+ const data = await response.json().catch(() => ({}));
4076
+ if (!response.ok || data.error) throw new Error(data.error || 'Codex account request failed.');
4077
+ return data;
4070
4078
  }
4071
4079
 
4072
- async function addCodexAccount() {
4073
- const tab = activeTab();
4074
- if (!tab) return;
4075
- if (tab.running) { appendSystem('Finish or stop the current turn before adding an account.', 'error'); return; }
4076
- try {
4077
- setStatus('Starting ChatGPT login…', 'working');
4078
- const result = await requestSocketPayloadForTab(tab, 'account_add_start', {});
4079
- if (Array.isArray(result?.accounts)) _codexAccounts = result.accounts;
4080
- const authUrl = result?.login?.login?.authUrl || result?.login?.authUrl;
4081
- if (authUrl) window.open(authUrl, '_blank', 'noopener,noreferrer');
4082
- if (isActiveTab(tab)) syncAccountChip();
4083
- } catch (err) {
4084
- if (!tab.closed) withTab(tab, () => {
4085
- appendSystem(err.message || 'Could not start login', 'error');
4086
- setStatus(err.message || 'Login failed', 'error');
4087
- });
4080
+ // Pick an account for a tab: the tab switches before its first message; a tab with a conversation keeps it
4081
+ // and the other account opens in a new tab.
4082
+ function pickCodexAccount(tab, id) {
4083
+ const plan = accountSwitchPlan({ rows: _codexAccounts, currentId: tab.accountId || 'default', targetId: id, hasConversation: !!tab.threadId, provider: 'codex' });
4084
+ if (plan.effect === 'missing' || plan.effect === 'same') { withTab(tab, () => appendSystem(plan.message, 'muted')); return; }
4085
+ if (plan.effect === 'next-chat') {
4086
+ const fresh = createTab({ project: tab.project || '', accountId: id });
4087
+ if (fresh) withTab(fresh, () => appendSystem(accountText('newChatUnder', { label: accountLabelFor(id) }, 'codex'), 'muted'));
4088
+ return;
4088
4089
  }
4090
+ if (tab.running) { withTab(tab, () => appendSystem(accountText('busyTurn', {}, 'codex'), 'error')); return; }
4091
+ setStatus('Switching account…', 'working');
4092
+ requestSocketPayloadForTab(tab, 'account_switch', { accountId: id })
4093
+ .then(() => { if (!tab.closed) withTab(tab, () => appendSystem(plan.message, 'muted')); })
4094
+ .catch((err) => {
4095
+ if (!tab.closed) withTab(tab, () => {
4096
+ appendSystem(err.message || 'Could not switch account', 'error');
4097
+ setStatus(err.message || 'Switch failed', 'error');
4098
+ });
4099
+ });
4089
4100
  }
4090
4101
 
4102
+ // Remove an account. The server refuses while a tab (this window's or another's), the Assistant or a run
4103
+ // uses it, with what to do: the menu says it. A conversation is never reset to free an account.
4091
4104
  async function removeCodexAccount(accountId) {
4092
- if (accountId === 'default') return;
4093
- if (!window.confirm('Remove this ChatGPT account from SynaBun? You can re-add it later by signing in again.')) return;
4094
- const tab = activeTab();
4095
- if (!tab) return;
4096
- const wasActive = tab && (tab.accountId || 'default') === accountId;
4097
- try {
4098
- const result = await requestSocketPayloadForTab(tab, 'account_remove', { accountId });
4099
- if (Array.isArray(result?.accounts)) _codexAccounts = result.accounts;
4100
- if (wasActive && !tab.closed) {
4101
- withTab(tab, () => {
4102
- _accountId = 'default';
4103
- tab.accountId = 'default';
4104
- saveTabs();
4105
- resetViewForAccountChange('Switched to Default');
4106
- });
4107
- }
4108
- if (isActiveTab(tab)) syncAccountChip();
4109
- const menu = panelEl('#cxp-account-menu');
4110
- if (menu?.classList.contains('open')) renderAccountMenu();
4111
- } catch (err) {
4112
- if (!tab.closed) withTab(tab, () => appendSystem(err.message || 'Could not remove account', 'error'));
4113
- }
4105
+ await codexAccountRequest(`/api/codex/accounts/${encodeURIComponent(accountId)}`, { method: 'DELETE' });
4106
+ }
4107
+
4108
+ function ensureCodexTerminalAuth(tab) {
4109
+ const accountId = tab.accountId || 'default';
4110
+ if (tab.closed || !tab.messagesEl) return;
4111
+ if (tab.terminalAuthView && tab.terminalAuthAccount === accountId) { void tab.terminalAuthView.refresh(); return; }
4112
+ tab.terminalAuthView?.dispose();
4113
+ tab.terminalAuthAccount = accountId;
4114
+ const named = accountId !== 'default' ? _codexAccounts.find((a) => a.id === accountId) : null;
4115
+ tab.terminalAuthView = mountProviderTerminalAuth({
4116
+ host: tab.messagesEl, provider: 'codex', accountId, logo: OPENAI_ICON,
4117
+ ...(named ? { message: `Sign in to Codex to use ${accountName(named, 'codex')}.` } : {}),
4118
+ // Another account (or a new one) is one click away from the sign-in screen: the header account menu.
4119
+ moreAction: { label: accountText('otherAccounts', {}, 'codex'), run: () => { if (isActiveTab(tab)) openAccountMenu(); } },
4120
+ isCurrent: () => !tab.closed && (tab.accountId || 'default') === accountId,
4121
+ // The account as the server reads it (its ChatGPT sign-in and the key it holds), not one app-server's view.
4122
+ readAccount: async () => {
4123
+ const result = await codexAccountRequest(`/api/codex/accounts/${encodeURIComponent(accountId)}/auth?refresh=true`);
4124
+ const row = result.account;
4125
+ if (row?.id) {
4126
+ const index = _codexAccounts.findIndex((a) => a.id === row.id);
4127
+ if (index >= 0) _codexAccounts[index] = row; else _codexAccounts.push(row);
4128
+ if (isActiveTab(tab)) syncAccountChip();
4129
+ }
4130
+ return signInStateOf(row);
4131
+ },
4132
+ requestApiKey: (provider, id, key) => providerApiKeyRequest(provider, id, key),
4133
+ onSignedIn: () => { invalidateCodexFeatureCaches(tab); refreshModelCatalog(tab); refreshCodexAccounts().catch(() => {}); },
4134
+ });
4114
4135
  }
4115
4136
 
4116
4137
  export async function renderSessionMenu() {
@@ -4970,7 +4991,7 @@ export function syncInputEnabled() {
4970
4991
  if (send) {
4971
4992
  const isStopMode = !!(tab?.running || _running);
4972
4993
  send.classList.toggle('cxp-send-stopping', !!isStopMode);
4973
- send.title = actionBlockedByCli ? 'Codex CLI not installed' : (isStopMode ? 'Stop' : 'Send');
4994
+ send.title = actionBlockedByCli ? codexCliBlockedText() : (isStopMode ? 'Stop' : 'Send');
4974
4995
  send.disabled = actionBlockedByCli || (isStopMode ? false : (!_connected || !_bootstrapped || _running || _startingThread || blockedByRequest || (!hasText && !hasImages && !hasPaths && !hasMentions)));
4975
4996
  }
4976
4997
  if (fresh) {
@@ -5268,6 +5289,12 @@ function noteCodexRevert(event) {
5268
5289
 
5269
5290
  // ── Notification handler (lines 7538-7698) ──
5270
5291
  export function handleNotify(method, params) {
5292
+ // Codex's own account changed: the server's reading of the account (its ChatGPT sign-in and the
5293
+ // key SynaBun holds) decides what the tab shows, so the sign-in screen and the menu ask it again.
5294
+ if (method === 'account/updated') {
5295
+ void _boundTab?.terminalAuthView?.refresh();
5296
+ refreshCodexAccounts().catch(() => {});
5297
+ }
5271
5298
  if (['skills/changed', 'app/list/updated', 'account/updated', 'account/rateLimits/updated', 'config/updated', 'thread/attachment/updated', 'thread/settings/updated', 'account/gatewayOAuth/changed'].includes(method)) {
5272
5299
  invalidateCodexFeatureCaches(_boundTab);
5273
5300
  }
@@ -5632,7 +5659,7 @@ export function handleNotify(method, params) {
5632
5659
  break;
5633
5660
  }
5634
5661
  case 'configWarning': {
5635
- appendSystem(formatCodexConfigWarning(params, method), 'error');
5662
+ showCodexConfigWarning(params);
5636
5663
  break;
5637
5664
  }
5638
5665
  case 'deprecationNotice':
@@ -6339,7 +6366,7 @@ function formatStatusSnapshotMarkdown(snapshot, tab = activeTab()) {
6339
6366
  `- Protocol baseline: \`${runtime.protocolBaseline || 'not reported'}\``,
6340
6367
  ];
6341
6368
  if (account) {
6342
- lines.push(`- Account: \`${account.email || account.name || account.type || 'signed in'}\`${account.planType ? ` (${account.planType})` : ''}`);
6369
+ lines.push(`- Account: \`${account.type === 'apiKey' ? 'API key' : account.email || account.name || account.type || 'signed in'}\`${account.planType ? ` (${account.planType})` : ''}`);
6343
6370
  } else {
6344
6371
  lines.push('- Account: signed out');
6345
6372
  }
@@ -6742,6 +6769,15 @@ async function handleLocalSlashCommand(payload = currentComposerPayload()) {
6742
6769
  invalidateCodexFeatureCaches(tab);
6743
6770
  appendMarkdown('Logged out of Codex.');
6744
6771
  break;
6772
+ case 'login':
6773
+ // Sign-in lives in the header account menu: this tab's account, its two ways to sign in.
6774
+ consumeComposer(input, tab, { clearAttachments: false });
6775
+ openAccountMenu({ manage: serverAccountId(tab.accountId) });
6776
+ break;
6777
+ case 'account':
6778
+ consumeComposer(input, tab, { clearAttachments: false });
6779
+ openAccountMenu();
6780
+ break;
6745
6781
  case 'diff':
6746
6782
  consumeComposer(input, tab, { clearAttachments: false });
6747
6783
  if (!tab.project) {
@@ -6802,6 +6838,13 @@ async function handleLocalSlashCommand(payload = currentComposerPayload()) {
6802
6838
  return true;
6803
6839
  }
6804
6840
 
6841
+ // Why Send is off while the CLI banner shows (ui-cli-banner.js): the banner's own title, in the page's
6842
+ // language — not installed, being installed, or failed to start.
6843
+ function codexCliBlockedText() {
6844
+ const key = isCliInstalling('codex') ? 'titleInstalling' : getCliState('codex') === 'broken' ? 'titleBroken' : 'titleMissing';
6845
+ return cliInstallText(key, { label: 'Codex' });
6846
+ }
6847
+
6805
6848
  // ── Send prompt & queue (lines 8165-8320) ──
6806
6849
  export function sendPrompt() {
6807
6850
  const payload = currentComposerPayload();
@@ -6811,7 +6854,7 @@ export function sendPrompt() {
6811
6854
  // Block when CLI binary missing — banner already informs user.
6812
6855
  const panel = document.getElementById('codex-panel');
6813
6856
  if (panel?.classList.contains('cxp-cli-blocked')) {
6814
- appendSystem('Codex CLI not installed. Install it first, then click Re-check.', 'error');
6857
+ appendSystem(`${codexCliBlockedText()}.`, 'error');
6815
6858
  return;
6816
6859
  }
6817
6860
  handleLocalSlashCommand(payload).then((handled) => {
@@ -7053,6 +7096,13 @@ export function renderImageStrip() {
7053
7096
  }
7054
7097
 
7055
7098
  // ── Whiteboard "Send to Panel" — receive image via event bus ──
7099
+ // Any window or surface changed a Codex account (signed in or out, a key, its mode, renamed, added,
7100
+ // removed): the menu re-reads the list, and the tabs on that account re-check sign-in.
7101
+ appOn('sync:codex:accounts-changed', (msg = {}) => {
7102
+ refreshCodexAccounts().catch(() => {});
7103
+ for (const tab of _tabs) if (!msg.accountId || serverAccountId(tab.accountId) === msg.accountId) void tab.terminalAuthView?.refresh();
7104
+ });
7105
+
7056
7106
  appOn('wb:send-to-panel', ({ dataUrl }) => {
7057
7107
  if (!dataUrl) return;
7058
7108
  if (appState.lastActivePanel !== 'codex' || isHostedSessionFocused()) return;
@@ -7447,512 +7497,59 @@ export function confirmSlashHint() {
7447
7497
  }
7448
7498
 
7449
7499
  // ── Phase 6: Settings panel ──
7450
- export async function openSettingsPanel({ section = '' } = {}) {
7500
+ function clearCodexConfigWarnings({ obsoleteOnly = false } = {}) {
7501
+ _getPanelEl()?.querySelectorAll('.cxp-config-warning').forEach(node => {
7502
+ if (!obsoleteOnly || /features\.personality/.test(node.textContent || '')) node.remove();
7503
+ });
7504
+ // Remove historical copies of the obsolete Settings warning as well.
7505
+ _getPanelEl()?.querySelectorAll('.cxp-system.error').forEach(node => {
7506
+ if (/features\.personality.*ignored/s.test(node.textContent || '')) node.remove();
7507
+ });
7508
+ }
7509
+
7510
+ function showCodexConfigWarning(params) {
7511
+ clearCodexConfigWarnings();
7512
+ const warning = document.createElement('div'); warning.className = 'cxp-config-warning'; warning.setAttribute('role', 'status');
7513
+ const text = document.createElement('div'); text.textContent = formatCodexConfigWarning(params);
7514
+ const hint = document.createElement('div'); hint.textContent = t('codexSettings.warningHelp');
7515
+ const settings = document.createElement('button'); settings.className = 'cxp-btn cxp-btn-sm'; settings.textContent = t('codexSettings.openSettings'); settings.onclick = () => openSettingsPanel();
7516
+ const dismiss = document.createElement('button'); dismiss.className = 'cxp-btn cxp-btn-sm'; dismiss.textContent = t('codexSettings.dismiss'); dismiss.onclick = () => warning.remove();
7517
+ warning.append(text, hint, settings, dismiss);
7518
+ _getPanelEl()?.querySelector('.cxp-input-wrap')?.before(warning);
7519
+ }
7451
7520
 
7452
- // ── Settings panel & compaction (lines 8588-8844) ──
7521
+ export async function openSettingsPanel({ section = '' } = {}) {
7453
7522
  const settingsTab = activeTab();
7454
7523
  if (!settingsTab) return;
7455
7524
  const settingsOwner = { accountId: settingsTab.accountId, threadId: settingsTab.threadId, connectionEpoch: settingsTab.connectionEpoch };
7456
- const requestForSettings = (type, data = {}) => requestForCodexOwner(settingsTab, settingsOwner, type, data,
7457
- 'This conversation changed. Reopen Settings to continue.');
7458
- const appendSettingsError = (text) => {
7459
- if (!settingsTab.closed) withTab(settingsTab, () => appendSystem(text, 'error'));
7460
- };
7461
- if (_getPanelEl()?.querySelector('.cxp-settings-overlay')) return;
7462
- const overlay = document.createElement('div');
7463
- overlay.className = 'cxp-settings-overlay';
7464
- overlay.innerHTML = `
7465
- <div class="cxp-settings-panel">
7466
- <div class="cxp-settings-header">
7467
- <span>Codex Settings</span>
7468
- <button class="cxp-settings-close">&times;</button>
7469
- </div>
7470
- <div class="cxp-settings-body">
7471
- <div class="cxp-settings-section">
7472
- <div class="cxp-settings-section-title">Account</div>
7473
- <div class="cxp-settings-loading" id="cxp-settings-account">Loading…</div>
7474
- </div>
7475
- <div class="cxp-settings-section">
7476
- <div class="cxp-settings-section-title">Runtime and features</div>
7477
- <div class="cxp-settings-loading" id="cxp-settings-runtime">Loading…</div>
7478
- <div id="cxp-settings-features"></div>
7479
- </div>
7480
- <div class="cxp-settings-section">
7481
- <div class="cxp-settings-section-title">MCP Servers</div>
7482
- <div class="cxp-settings-loading" id="cxp-settings-mcp">Loading…</div>
7483
- </div>
7484
- <div class="cxp-settings-section">
7485
- <div class="cxp-settings-section-title">Always Allowed Folders</div>
7486
- <div class="cxp-settings-loading" id="cxp-settings-permissions">Loading…</div>
7487
- </div>
7488
- <div class="cxp-settings-section">
7489
- <div class="cxp-settings-section-title">Configuration</div>
7490
- <div class="cxp-settings-loading" id="cxp-settings-config">Loading…</div>
7491
- </div>
7492
- </div>
7493
- </div>
7494
- `;
7495
- _getPanelEl().appendChild(overlay);
7496
- overlay.querySelector('.cxp-settings-close')?.addEventListener('click', () => overlay.remove());
7497
- overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
7498
-
7499
- // Fetch data in parallel
7500
- try {
7501
- const [accountData, mcpData, configData, rateLimitData, permissionData, requirementsData, capabilityData] = await Promise.allSettled([
7502
- requestForSettings('account_read', {}),
7503
- requestForSettings('mcp_status', {}),
7504
- requestForSettings('config_read', { cwd: settingsTab.project || null, threadId: settingsTab.threadId || null }),
7505
- requestForSettings('rate_limits_read', {}),
7506
- requestForSettings('permission_roots_list', {}),
7507
- requestForSettings('config_requirements', {}),
7508
- getCodexCapabilities(settingsTab),
7509
- ]);
7510
- if (settingsTab.closed) {
7511
- overlay.remove();
7512
- return;
7513
- }
7514
- const capabilities = capabilityData.status === 'fulfilled' ? capabilityData.value : {};
7515
- const runtime = settingsTab.codexRuntime || {};
7516
- const runtimeEl = overlay.querySelector('#cxp-settings-runtime');
7517
- runtimeEl.className = 'cxp-settings-hint';
7518
- runtimeEl.textContent = `CLI ${runtime.cliVersion || 'unknown'} · Automation SDK ${runtime.sdkVersion || 'not installed'} · Protocol ${runtime.protocolBaseline || 'unknown'}\n${runtime.codexBin || 'Executable path not reported'}\nAccount: ${runtime.accountId || settingsTab.accountId || 'default'}`;
7519
- const featureEl = overlay.querySelector('#cxp-settings-features');
7520
- for (const kind of ['skills', 'mcp', 'apps', 'plugins', 'hooks', 'ps', 'experimental', 'goal', 'feedback']) {
7521
- const command = CODEX_COMMAND_BY_NAME.get(kind);
7522
- const available = capabilityAvailability(capabilities, command.capability);
7523
- const btn = document.createElement('button');
7524
- btn.className = 'cxp-btn cxp-btn-sm';
7525
- btn.textContent = `${command.name}${available.experimental ? ' · Experimental' : ''}`;
7526
- btn.disabled = !available.supported || (['goal', 'ps'].includes(kind) && !settingsTab.threadId);
7527
- btn.title = !available.supported ? available.reason : command.desc;
7528
- btn.addEventListener('click', async () => {
7529
- const context = {
7530
- host: _getPanelEl(), request: requestForSettings, tab: settingsTab, capabilities,
7531
- authenticate: (name) => startMcpOAuthLogin(settingsTab, name),
7532
- onChanged: () => invalidateCodexFeatureCaches(settingsTab),
7533
- };
7534
- try {
7535
- if (kind === 'goal') await openCodexGoal(context);
7536
- else if (kind === 'feedback') await openCodexFeedback(context);
7537
- else await openCodexCatalog({ ...context, kind });
7538
- } catch (error) { appendSettingsError(error.message); }
7539
- });
7540
- featureEl.append(btn);
7541
- if (!available.supported) {
7542
- const reason = document.createElement('div'); reason.className = 'cxp-settings-hint';
7543
- reason.textContent = `${command.name}: ${available.reason || 'Unavailable in this runtime'}`; featureEl.append(reason);
7544
- }
7545
- }
7546
-
7547
- // Account
7548
- const accountEl = overlay.querySelector('#cxp-settings-account');
7549
- if (accountEl) {
7550
- const acctResult = accountData.status === 'fulfilled' ? (accountData.value?.account || {}) : {};
7551
- const acct = acctResult?.account || null;
7552
- const acctName = acct?.email || acct?.name || acct?.type;
7553
- const limitLines = rateLimitData.status === 'fulfilled'
7554
- ? rateLimitLines(rateLimitData.value?.rateLimits || {})
7555
- : [];
7556
- if (acctName) {
7557
- accountEl.innerHTML = '';
7558
- accountEl.className = '';
7559
- const value = document.createElement('div');
7560
- value.className = 'cxp-settings-value';
7561
- value.textContent = acctName;
7562
- accountEl.appendChild(value);
7563
- if (acct?.planType) {
7564
- const plan = document.createElement('div');
7565
- plan.className = 'cxp-settings-hint';
7566
- plan.textContent = `Plan: ${acct.planType}`;
7567
- accountEl.appendChild(plan);
7568
- }
7569
- if (limitLines.length) {
7570
- const limits = document.createElement('div');
7571
- limits.className = 'cxp-settings-hint';
7572
- limits.innerHTML = limitLines.map((line) => `<div>${esc(line.replace(/^- /, ''))}</div>`).join('');
7573
- accountEl.appendChild(limits);
7574
- }
7575
- const logoutBtn = document.createElement('button');
7576
- logoutBtn.className = 'cxp-settings-login-btn';
7577
- logoutBtn.textContent = 'Logout';
7578
- logoutBtn.addEventListener('click', async () => {
7579
- logoutBtn.disabled = true;
7580
- try {
7581
- await requestForSettings('account_logout', {});
7582
- invalidateCodexFeatureCaches(settingsTab);
7583
- overlay.remove();
7584
- if (isActiveTab(settingsTab)) openSettingsPanel();
7585
- } catch (err) {
7586
- logoutBtn.disabled = false;
7587
- appendSettingsError(err.message || 'Logout failed');
7588
- }
7589
- });
7590
- accountEl.appendChild(logoutBtn);
7591
- } else {
7592
- accountEl.innerHTML = '';
7593
- accountEl.className = '';
7594
- const hint = document.createElement('div');
7595
- hint.className = 'cxp-settings-hint';
7596
- hint.textContent = 'Not signed in';
7597
- const loginBtn = document.createElement('button');
7598
- loginBtn.className = 'cxp-settings-login-btn';
7599
- loginBtn.textContent = 'Login';
7600
- loginBtn.addEventListener('click', async () => {
7601
- loginBtn.disabled = true;
7602
- loginBtn.textContent = 'Opening…';
7603
- try {
7604
- const result = await requestForSettings('codex_login', {});
7605
- const authUrl = result?.login?.login?.authUrl || result?.login?.authUrl;
7606
- if (authUrl) window.open(authUrl, '_blank', 'noopener,noreferrer');
7607
- loginBtn.textContent = authUrl ? 'Browser opened' : 'Waiting…';
7608
- } catch {
7609
- loginBtn.textContent = 'Login';
7610
- loginBtn.disabled = false;
7611
- }
7612
- });
7613
- accountEl.append(hint, loginBtn);
7614
- }
7615
- }
7616
-
7617
- await mountCodexAccountSecurity({ host: accountEl, request: requestForSettings, tab: settingsTab, capabilities });
7618
-
7619
- // MCP Servers
7620
- const mcpEl = overlay.querySelector('#cxp-settings-mcp');
7621
- if (mcpEl) {
7622
- const servers = mcpData.status === 'fulfilled' ? (mcpData.value?.servers || []) : [];
7623
- if (!servers.length) {
7624
- mcpEl.innerHTML = '';
7625
- mcpEl.className = '';
7626
- const hint = document.createElement('div');
7627
- hint.className = 'cxp-settings-hint';
7628
- hint.textContent = 'Configure in ~/.codex/config.toml under [mcp_servers]';
7629
- mcpEl.appendChild(hint);
7630
- } else {
7631
- mcpEl.innerHTML = '';
7632
- for (const server of servers) {
7633
- const row = document.createElement('div');
7634
- row.className = 'cxp-mcp-row';
7635
- const name = document.createElement('span');
7636
- name.className = 'cxp-mcp-name';
7637
- name.textContent = server.name || 'Unknown';
7638
- const status = document.createElement('span');
7639
- status.className = 'cxp-mcp-status';
7640
- status.textContent = server.status || 'unknown';
7641
- status.dataset.status = server.status || 'unknown';
7642
- const trusted = document.createElement('span');
7643
- trusted.className = 'cxp-mcp-status';
7644
- trusted.dataset.status = 'running';
7645
- trusted.textContent = 'always allowed';
7646
- trusted.hidden = !server.alwaysAllowed;
7647
- const refreshBtn = document.createElement('button');
7648
- refreshBtn.className = 'cxp-btn cxp-btn-sm';
7649
- refreshBtn.textContent = 'Refresh';
7650
- refreshBtn.addEventListener('click', async () => {
7651
- try {
7652
- await requestForSettings('mcp_refresh', { name: server.name });
7653
- refreshBtn.textContent = 'Done';
7654
- setTimeout(() => { refreshBtn.textContent = 'Refresh'; }, 2000);
7655
- } catch (err) {
7656
- appendSettingsError(`MCP refresh failed: ${err.message}`);
7657
- }
7658
- });
7659
- row.append(name, status, trusted, refreshBtn);
7660
- if (server.alwaysAllowed) {
7661
- const revokeBtn = document.createElement('button');
7662
- revokeBtn.className = 'cxp-btn cxp-btn-sm';
7663
- revokeBtn.textContent = 'Revoke';
7664
- revokeBtn.title = 'Require approval for this MCP server again';
7665
- revokeBtn.addEventListener('click', async () => {
7666
- revokeBtn.disabled = true;
7667
- try {
7668
- await requestForSettings('mcp_approval_set', {
7669
- name: server.name,
7670
- approvalMode: 'prompt',
7671
- });
7672
- server.approvalMode = 'prompt';
7673
- server.alwaysAllowed = false;
7674
- trusted.hidden = true;
7675
- revokeBtn.remove();
7676
- } catch (err) {
7677
- revokeBtn.disabled = false;
7678
- appendSettingsError(`MCP approval update failed: ${err.message}`);
7679
- }
7680
- });
7681
- row.appendChild(revokeBtn);
7682
- }
7683
- if (server.requiresOAuth) {
7684
- const loginBtn = document.createElement('button');
7685
- loginBtn.className = 'cxp-btn cxp-btn-sm cxp-mcp-oauth-btn';
7686
- loginBtn.dataset.mcpServer = server.name;
7687
- loginBtn.textContent = 'Authenticate';
7688
- loginBtn.addEventListener('click', async () => {
7689
- try {
7690
- await startMcpOAuthLogin(settingsTab, server.name);
7691
- } catch (err) {
7692
- appendSettingsError(`MCP OAuth failed: ${err.message}`);
7693
- }
7694
- });
7695
- row.appendChild(loginBtn);
7696
- }
7697
- mcpEl.appendChild(row);
7698
- }
7699
- }
7700
- }
7701
-
7702
- const permissionsEl = overlay.querySelector('#cxp-settings-permissions');
7703
- if (permissionsEl) {
7704
- const stored = permissionData.status === 'fulfilled'
7705
- ? permissionData.value?.permissions?.filesystem || {}
7706
- : {};
7707
- const roots = ['write', 'read'].flatMap((access) => (
7708
- (Array.isArray(stored[access]) ? stored[access] : []).map((path) => ({ access, path }))
7709
- ));
7710
- permissionsEl.innerHTML = '';
7711
- permissionsEl.className = '';
7712
- if (!roots.length) {
7713
- const hint = document.createElement('div');
7714
- hint.className = 'cxp-settings-hint';
7715
- hint.textContent = 'None';
7716
- permissionsEl.appendChild(hint);
7717
- } else {
7718
- for (const root of roots) {
7719
- const row = document.createElement('div');
7720
- row.className = 'cxp-mcp-row';
7721
- const path = document.createElement('span');
7722
- path.className = 'cxp-mcp-name cxp-permission-root-path';
7723
- path.textContent = root.path;
7724
- path.title = root.path;
7725
- const access = document.createElement('span');
7726
- access.className = 'cxp-mcp-status';
7727
- access.textContent = root.access;
7728
- const remove = document.createElement('button');
7729
- remove.type = 'button';
7730
- remove.className = 'cxp-bar-action';
7731
- remove.innerHTML = ICON_X;
7732
- remove.title = 'Revoke folder access';
7733
- remove.setAttribute('aria-label', `Revoke access to ${root.path}`);
7734
- remove.addEventListener('click', async () => {
7735
- remove.disabled = true;
7736
- try {
7737
- await requestForSettings('permission_root_remove', root);
7738
- row.remove();
7739
- if (!permissionsEl.querySelector('.cxp-mcp-row')) {
7740
- const hint = document.createElement('div');
7741
- hint.className = 'cxp-settings-hint';
7742
- hint.textContent = 'None';
7743
- permissionsEl.appendChild(hint);
7744
- }
7745
- } catch (err) {
7746
- remove.disabled = false;
7747
- appendSettingsError(err.message || 'Could not revoke folder access');
7748
- }
7749
- });
7750
- row.append(path, access, remove);
7751
- permissionsEl.appendChild(row);
7752
- }
7753
- }
7754
- }
7755
-
7756
- // Config — editable fields
7757
- const configEl = overlay.querySelector('#cxp-settings-config');
7758
- if (configEl) {
7759
- const rawCfg = configData.status === 'fulfilled' ? (configData.value?.config || configData.value || {}) : {};
7760
- const { effective: cfg, layers } = normalizeConfigResult(rawCfg);
7761
- if (configData.status === 'fulfilled' && configData.value?.sessionOverrides) {
7762
- settingsTab.sessionConfigOverrides = Object.fromEntries(Object.entries(SESSION_CONFIG_FIELDS)
7763
- .filter(([, field]) => Object.hasOwn(configData.value.sessionOverrides, field))
7764
- .map(([key, field]) => [key, configData.value.sessionOverrides[field]]));
7765
- }
7766
- const requirements = requirementsData.status === 'fulfilled' ? requirementsData.value : {};
7767
-
7768
- // Get dynamic model list (fall back to hardcoded defaults)
7769
- let models;
7770
- try { models = await requestModelList(settingsTab); } catch { models = null; }
7771
- const modelList = mergeCodexModelList(models);
7772
-
7773
- configEl.innerHTML = '';
7774
- const fieldDefs = [
7775
- { key: 'model', label: 'Model', type: 'select', options: modelList.map(m => ({ value: getCodexModelName(m), label: m?.label || getCodexModelName(m) })) },
7776
- { key: 'review_model', label: 'Review Model', type: 'select', options: [{ value: '', label: '(none)' }, ...modelList.map(m => ({ value: getCodexModelName(m), label: m?.label || getCodexModelName(m) }))] },
7777
- { key: 'model_provider', label: 'Provider', type: 'text', placeholder: 'openai, ollama…' },
7778
- { key: 'approval_policy', label: 'Approval Policy', type: 'select', options: [
7779
- { value: 'never', label: 'Never (No prompts)' },
7780
- { value: 'untrusted', label: 'Untrusted' },
7781
- { value: 'on-request', label: 'On Request' },
7782
- ]},
7783
- { key: 'sandbox_mode', label: 'Sandbox Mode', type: 'select', options: [
7784
- { value: 'workspace-write', label: 'Workspace Write' },
7785
- { value: 'read-only', label: 'Read Only' },
7786
- { value: 'danger-full-access', label: 'Full Access (Dangerous)' },
7787
- ]},
7788
- { key: 'personality', label: 'Personality', type: 'select', options: [
7789
- { value: '', label: '(default)' },
7790
- { value: 'pragmatic', label: 'Pragmatic' },
7791
- { value: 'friendly', label: 'Friendly' },
7792
- ]},
7793
- { key: 'service_tier', label: 'Service Tier', type: 'select', options: [
7794
- { value: '', label: '(default)' },
7795
- ]},
7796
- { key: 'model_reasoning_effort', label: 'Reasoning Effort', type: 'select', options: [
7797
- { value: '', label: '(default)' },
7798
- { value: 'minimal', label: 'Minimal' },
7799
- { value: 'low', label: 'Low' },
7800
- { value: 'medium', label: 'Medium' },
7801
- { value: 'high', label: 'High' },
7802
- { value: 'xhigh', label: 'Extra High' },
7803
- { value: 'max', label: 'Maximum' },
7804
- { value: 'ultra', label: 'Ultra' },
7805
- ]},
7806
- { key: 'model_reasoning_summary', label: 'Reasoning Summary', type: 'select', options: [
7807
- { value: '', label: '(default)' },
7808
- { value: 'auto', label: 'Auto' },
7809
- { value: 'concise', label: 'Concise' },
7810
- { value: 'detailed', label: 'Detailed' },
7811
- { value: 'none', label: 'None' },
7812
- ]},
7813
- { key: 'model_verbosity', label: 'Response Verbosity', type: 'select', options: [
7814
- { value: '', label: '(default)' },
7815
- { value: 'low', label: 'Low' },
7816
- { value: 'medium', label: 'Medium' },
7817
- { value: 'high', label: 'High' },
7818
- ]},
7819
- { key: 'model_context_window', label: 'Context Window', type: 'number', placeholder: 'tokens' },
7820
- { key: 'model_auto_compact_token_limit', label: 'Auto-Compact', type: 'number', placeholder: 'token limit' },
7821
- { key: 'web_search', label: 'Web Search', type: 'select', options: [
7822
- { value: '', label: '(default)' },
7823
- { value: 'disabled', label: 'Disabled' },
7824
- { value: 'cached', label: 'Cached' },
7825
- { value: 'indexed', label: 'Indexed' },
7826
- { value: 'live', label: 'Live' },
7827
- ] },
7828
- { key: 'features.fast_mode', label: 'Enable Fast Toggle', type: 'checkbox' },
7829
- { key: 'features.personality', label: 'Enable Personality', type: 'checkbox' },
7830
- { key: 'features.apps', label: 'Enable Apps', type: 'checkbox' },
7831
- { key: 'features.unified_exec', label: 'Unified Exec', type: 'checkbox' },
7832
- ];
7833
- const effortDefinition = fieldDefs.find((def) => def.key === 'model_reasoning_effort');
7834
- const modelMetadata = selectedModelOption(modelList, cfg.model || settingsTab.model);
7835
- if (modelMetadata) effortDefinition.options = normalizeReasoningEfforts(modelMetadata).map((option) => ({ value: option.id === 'off' ? '' : option.id, label: option.label || option.id }));
7836
- fieldDefs.find((def) => def.key === 'service_tier').options.push(...modelServiceTiers(modelMetadata));
7837
- const form = document.createElement('div');
7838
- form.className = 'cxp-config-form';
7839
- const scopeLabel = document.createElement('label');
7840
- scopeLabel.className = 'cxp-config-row';
7841
- scopeLabel.textContent = 'Apply to ';
7842
- const scope = document.createElement('select');
7843
- scope.className = 'cxp-config-input';
7844
- const savedOption = document.createElement('option'); savedOption.value = 'saved'; savedOption.textContent = 'Saved defaults';
7845
- const sessionOption = document.createElement('option'); sessionOption.value = 'session'; sessionOption.textContent = 'Current thread';
7846
- sessionOption.disabled = !settingsTab.threadId || !capabilityAvailability(capabilities, 'thread_settings_update').supported;
7847
- scope.append(savedOption, sessionOption); scopeLabel.append(scope); form.append(scopeLabel);
7848
- const scopeHint = document.createElement('div'); scopeHint.className = 'cxp-settings-hint';
7849
- scopeHint.textContent = 'Saved defaults affect future sessions. Current thread settings apply to subsequent turns.';
7850
- form.append(scopeHint);
7851
- const inputs = {};
7852
- const restrictions = {};
7853
- for (const def of fieldDefs) {
7854
- const row = document.createElement('div');
7855
- row.className = 'cxp-config-row';
7856
- const label = document.createElement('label');
7857
- label.className = 'cxp-config-label';
7858
- label.textContent = def.label;
7859
- row.appendChild(label);
7860
- const val = configValue(cfg, def.key);
7861
- const restriction = configRestriction(requirements, def.key);
7862
- restrictions[def.key] = restriction;
7863
- if (def.type === 'select') {
7864
- const sel = document.createElement('select');
7865
- sel.className = 'cxp-config-input';
7866
- for (const opt of def.options) {
7867
- const o = document.createElement('option');
7868
- o.value = opt.value;
7869
- o.textContent = opt.label;
7870
- if (String(val ?? '') === opt.value) o.selected = true;
7871
- if (restriction.allowed && opt.value && !restriction.allowed.some((allowed) => JSON.stringify(allowed) === JSON.stringify(opt.value))) o.disabled = true;
7872
- sel.appendChild(o);
7873
- }
7874
- if (val != null && !def.options.some((opt) => opt.value === String(val))) {
7875
- const unknown = document.createElement('option');
7876
- unknown.value = typeof val === 'object' ? JSON.stringify(val) : String(val);
7877
- unknown.textContent = `${unknown.value} (configured)`; unknown.selected = true;
7878
- sel.appendChild(unknown);
7879
- sel.dataset.originalValue = JSON.stringify(val);
7880
- }
7881
- row.appendChild(sel);
7882
- inputs[def.key] = sel;
7883
- } else if (def.type === 'checkbox') {
7884
- const cb = document.createElement('input');
7885
- cb.type = 'checkbox';
7886
- cb.className = 'cxp-config-checkbox';
7887
- cb.checked = typeof restriction.fixed === 'boolean' ? restriction.fixed : !!val;
7888
- row.appendChild(cb);
7889
- inputs[def.key] = cb;
7890
- } else {
7891
- const inp = document.createElement('input');
7892
- inp.type = def.type === 'number' ? 'number' : 'text';
7893
- inp.className = 'cxp-config-input';
7894
- inp.value = val != null ? val : '';
7895
- inp.placeholder = def.placeholder || '';
7896
- row.appendChild(inp);
7897
- inputs[def.key] = inp;
7898
- }
7899
- // Config layer badge
7900
- if (layers[def.key] || restriction.reason) {
7901
- const badge = document.createElement('span');
7902
- badge.className = 'cxp-config-layer';
7903
- badge.textContent = `(${restriction.reason || layers[def.key]})`;
7904
- row.appendChild(badge);
7905
- }
7906
- form.appendChild(row);
7525
+ const requestForSettings = (type, data = {}) => requestForCodexOwner(settingsTab, settingsOwner, type, data, t('codexSettings.changedConversation'));
7526
+ const capabilities = await getCodexCapabilities(settingsTab);
7527
+ if (settingsTab.closed || !isActiveTab(settingsTab)) return;
7528
+ const view = await mountCodexSettings({
7529
+ host: _getPanelEl(), tab: settingsTab,
7530
+ request: requestForSettings,
7531
+ capabilities,
7532
+ account: _codexAccounts.find(a => a.id === (settingsTab.accountId || 'default')),
7533
+ models: async () => (await requestModelList(settingsTab)) || [],
7534
+ normalizeConfig: normalizeConfigResult,
7535
+ manageAccount: () => { if (isActiveTab(settingsTab)) openAccountMenu({ manage: settingsTab.accountId || 'default' }); },
7536
+ authenticate: name => startMcpOAuthLogin(settingsTab, name),
7537
+ onSaved: ({ scope, patch, overrides }) => {
7538
+ settingsTab.sessionConfigOverrides = overrides;
7539
+ if (scope === 'session') {
7540
+ if (Object.hasOwn(patch, 'model')) settingsTab.model = patch.model;
7541
+ if (Object.hasOwn(patch, 'model_reasoning_effort')) settingsTab.effort = patch.model_reasoning_effort || 'off';
7542
+ if (Object.hasOwn(patch, 'approval_policy')) settingsTab.autoAccept = patch.approval_policy === 'never';
7543
+ syncToolbarState(); saveTabs();
7907
7544
  }
7908
- const saveBtn = document.createElement('button');
7909
- saveBtn.className = 'cxp-btn cxp-config-save';
7910
- const refreshScope = () => {
7911
- const visibleConfig = scope.value === 'session' ? { ...cfg, ...(settingsTab.sessionConfigOverrides || {}) } : cfg;
7912
- for (const def of fieldDefs) {
7913
- inputs[def.key].disabled = Object.hasOwn(restrictions[def.key], 'fixed') || (scope.value === 'session' && !SESSION_CONFIG_FIELDS[def.key]);
7914
- const value = configValue(visibleConfig, def.key);
7915
- if (def.type === 'checkbox') inputs[def.key].checked = typeof restrictions[def.key].fixed === 'boolean' ? restrictions[def.key].fixed : !!value;
7916
- else inputs[def.key].value = value != null ? (typeof value === 'object' ? JSON.stringify(value) : String(value)) : '';
7917
- }
7918
- saveBtn.textContent = scope.value === 'session' ? 'Apply to Current Thread' : 'Save Defaults';
7919
- };
7920
- scope.addEventListener('change', refreshScope);
7921
- refreshScope();
7922
- saveBtn.addEventListener('click', async () => {
7923
- saveBtn.disabled = true;
7924
- saveBtn.textContent = 'Saving…';
7925
- try {
7926
- const currentConfig = scope.value === 'session' ? { ...cfg, ...(settingsTab.sessionConfigOverrides || {}) } : cfg;
7927
- const newCfg = configChanges(fieldDefs, inputs, currentConfig);
7928
- if (scope.value === 'session') {
7929
- if (settingsTab.running) throw new Error('Wait for the active turn to finish before applying thread settings.');
7930
- await requestForSettings('thread_settings_update', { threadId: settingsTab.threadId, ...sessionSettings(newCfg) });
7931
- settingsTab.sessionConfigOverrides = { ...(settingsTab.sessionConfigOverrides || {}), ...newCfg };
7932
- if (Object.hasOwn(newCfg, 'model')) settingsTab.model = newCfg.model;
7933
- if (Object.hasOwn(newCfg, 'model_reasoning_effort')) settingsTab.effort = newCfg.model_reasoning_effort || 'off';
7934
- if (Object.hasOwn(newCfg, 'approval_policy')) settingsTab.autoAccept = newCfg.approval_policy === 'never';
7935
- syncToolbarState(); saveTabs();
7936
- } else if (Object.keys(newCfg).length) {
7937
- await requestForSettings('config_write', { config: newCfg });
7938
- Object.assign(cfg, newCfg);
7939
- }
7940
- invalidateCodexFeatureCaches(settingsTab);
7941
- saveBtn.textContent = 'Saved!';
7942
- refreshModelCatalog(settingsTab);
7943
- setTimeout(() => { refreshScope(); saveBtn.disabled = false; }, 1500);
7944
- } catch (err) {
7945
- saveBtn.textContent = 'Save Failed';
7946
- appendSettingsError(`Config save failed: ${err.message}`);
7947
- setTimeout(() => { refreshScope(); saveBtn.disabled = false; }, 1500);
7948
- }
7949
- });
7950
- form.appendChild(saveBtn);
7951
- configEl.appendChild(form);
7952
- }
7953
- // Opened for one section (the Context settings popover's "Manage"): bring it into view.
7954
- if (section) overlay.querySelector(`#cxp-settings-${section}`)?.closest('.cxp-settings-section')?.scrollIntoView({ block: 'start' });
7955
- } catch (error) { appendSettingsError(error.message || 'Could not load Codex settings.'); }
7545
+ invalidateCodexFeatureCaches(settingsTab); refreshModelCatalog(settingsTab);
7546
+ clearCodexConfigWarnings({ obsoleteOnly: true });
7547
+ },
7548
+ });
7549
+ if (section && view) {
7550
+ const target = section === 'mcp' ? view.sections.tools : view.sections[section];
7551
+ target?.scrollIntoView({ block: 'start' });
7552
+ }
7956
7553
  }
7957
7554
 
7958
7555
  export function startCompaction() {