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
@@ -0,0 +1,335 @@
1
+ // Claude and Codex accounts as every surface shows them: each side panel's
2
+ // header account menu (public/shared/account-menu.js), its sign-in screen, the
3
+ // Assistant's brain picker and accounts manager. The rows come from the server
4
+ // (lib/claude-accounts.js and lib/codex-accounts.js describe(): provider, mode,
5
+ // signedIn, identity, subscription, apiKey); this module only decides the words
6
+ // and what a choice does. No imports and no DOM: node --test loads it.
7
+ //
8
+ // ACCOUNT_COPY is the English; `accounts.*` in i18n/en.json must equal it and
9
+ // i18n/pt-BR.json translates it. A page hands its translator in once
10
+ // (setAccountTranslator); without one (a test, a detached window before i18n
11
+ // loads) the English is used.
12
+
13
+ export const ACCOUNT_COPY = Object.freeze({
14
+ subscriptionBadge: '{service} account',
15
+ apiKeyBadge: 'API key',
16
+ apiShort: 'API',
17
+ signedOut: 'Signed out',
18
+ checking: 'Checking…',
19
+ keyUnreadable: 'Key unreadable',
20
+ defaultName: 'Default',
21
+ current: 'This tab',
22
+ nowNote: 'Applies now: the next message runs under {label}.',
23
+ nextNote: 'This chat stays on {current}. {label} opens in a new chat.',
24
+ sameNote: 'This tab already uses {label}.',
25
+ missingNote: 'That account is no longer set up. Choose another one.',
26
+ menuNoteNow: 'The account you pick applies from the next message.',
27
+ menuNoteChat: 'This chat stays on {label}. Picking another account opens a new chat.',
28
+ newChatUnder: 'This chat runs under {label}.',
29
+ tooManyTabs: 'Close a tab first: the most tabs are open.',
30
+ menuTitle: '{provider} accounts',
31
+ triggerLabel: '{provider} account: {summary}',
32
+ manage: 'Manage {label}',
33
+ back: 'All accounts',
34
+ addAccount: 'Add account',
35
+ refresh: 'Refresh',
36
+ subscriptionShort: 'Subscription',
37
+ emptyAccounts: 'No accounts yet.',
38
+ usageLeft: '{percent}% left',
39
+ usageLimited: 'Limit reached',
40
+ usageResets: 'Resets {time}',
41
+ reading: 'Reading the accounts…',
42
+ readFailed: 'Could not read the accounts. Try again.',
43
+ needsServer: 'Choosing an account needs a newer SynaBun server: restart it to enable this.',
44
+ otherAccounts: 'Other accounts…',
45
+ uses: 'Uses',
46
+ usesGroup: 'What {label} uses',
47
+ credSubscription: '{service} subscription',
48
+ credApiKey: 'API key',
49
+ inUse: 'in use',
50
+ signedIn: 'Signed in',
51
+ notSignedIn: 'Not signed in',
52
+ notChecked: 'Not checked yet',
53
+ noKey: 'None',
54
+ savedKey: 'Saved',
55
+ keyUnreadableLong: 'Saved key cannot be read: save it again',
56
+ keyKeptBy: '{hint}, kept by {cli}',
57
+ signIn: 'Sign in with {service}',
58
+ useApiKey: 'Use an API key',
59
+ signOut: 'Sign out',
60
+ removeKey: 'Remove key',
61
+ rename: 'Rename',
62
+ remove: 'Remove account',
63
+ save: 'Save',
64
+ cancel: 'Cancel',
65
+ keyPlaceholder: 'Paste API key',
66
+ keyLabel: 'API key for {label}',
67
+ nameLabel: 'Account name',
68
+ freshPrompt: 'Sign {label} in: with {service} (a terminal opens) or with an API key.',
69
+ signInPending: 'Finish signing in in the terminal. This menu updates when {cli} confirms it.',
70
+ checkingKey: 'Checking the key with {vendor}…',
71
+ keySavedNote: '{label} now uses its API key.',
72
+ modeSetApiKey: '{label} now uses its API key, from the next message.',
73
+ modeSetSubscription: '{label} now uses its {service} account, from the next message.',
74
+ signedOutNote: '{label} is signed out of {service}.',
75
+ keyRemovedNote: 'The API key of {label} was removed.',
76
+ confirmSignOut: 'Sign {label} out of {service}? {cli} removes the subscription login from this account.',
77
+ confirmRemoveKey: 'Remove the API key saved for {label}?',
78
+ confirmRemove: 'Remove {label}? Its folder, its sign-in and its saved key are deleted.',
79
+ busyTurn: 'Finish or stop the current turn first.',
80
+ failed: 'That did not work. Try again.',
81
+ errInvalidKey: "That doesn't look like an {vendor} API key.",
82
+ errOauthToken: "That's a Claude sign-in token. Use Sign in with Claude.",
83
+ errKeyRejected: '{vendor} rejected that key.',
84
+ errPermission: 'Saving keys requires the owner’s SynaBun window. Reload and try again.',
85
+ errAccountUnavailable: 'This account is no longer available. Select an account again.',
86
+ errAccountBusy: 'Finish this account’s work, then try again.',
87
+ errUnreachable: "Couldn't reach {vendor} to check the key. Try again.",
88
+ errUnavailable: "{vendor} couldn't check the key. Try again shortly.",
89
+ errServer: "Couldn't reach SynaBun. Check the connection and try again.",
90
+ errStorage: "Couldn't save the key for this account.",
91
+ });
92
+
93
+ // The names each provider's sentences use.
94
+ const PROVIDER_WORDS = Object.freeze({
95
+ claude: Object.freeze({ provider: 'Claude', service: 'Claude', cli: 'Claude Code', vendor: 'Anthropic' }),
96
+ codex: Object.freeze({ provider: 'Codex', service: 'ChatGPT', cli: 'Codex', vendor: 'OpenAI' }),
97
+ });
98
+
99
+ let _translate = null;
100
+ /** A page's translator: (key, params) => the `accounts.<key>` sentence, or null/undefined/the key itself when it has none. */
101
+ export function setAccountTranslator(fn) { _translate = typeof fn === 'function' ? fn : null; }
102
+
103
+ const fill = (text, params = {}) => String(text).replace(/\{(\w+)\}/g, (whole, key) => (params[key] != null ? params[key] : whole));
104
+
105
+ /** The provider of a row ('claude' when it does not say: Claude rows came first). */
106
+ export function rowProvider(row, provider = null) {
107
+ const value = provider || row?.provider;
108
+ return value === 'codex' ? 'codex' : 'claude';
109
+ }
110
+ /** The names a provider's sentences use: { provider, service, cli, vendor }. */
111
+ export function providerWords(provider) {
112
+ return PROVIDER_WORDS[provider === 'codex' ? 'codex' : 'claude'];
113
+ }
114
+
115
+ /** One ACCOUNT_COPY sentence in the page's language, with the provider's names and `params` filled in. */
116
+ export function accountText(key, params = {}, provider = 'claude') {
117
+ const values = { ...providerWords(provider), ...params };
118
+ if (_translate) {
119
+ try {
120
+ const translated = _translate(key, values);
121
+ if (typeof translated === 'string' && translated && translated !== key && translated !== `accounts.${key}`) return fill(translated, values);
122
+ } catch { /* the English below */ }
123
+ }
124
+ return fill(Object.hasOwn(ACCOUNT_COPY, key) ? ACCOUNT_COPY[key] : key, values);
125
+ }
126
+
127
+ /** The id a Claude tab stores for an account ('' is Default, as Claude tabs have always stored it). */
128
+ export function tabAccountId(id) {
129
+ return !id || id === 'default' ? '' : String(id);
130
+ }
131
+ /** The id the server names an account by ('default' for Default). */
132
+ export function serverAccountId(id) {
133
+ return id ? String(id) : 'default';
134
+ }
135
+
136
+ /** What an account runs on now, as a short badge: { text, tone: ok | warn | muted, mode }. */
137
+ export function accountBadge(row, provider = null) {
138
+ const p = rowProvider(row, provider);
139
+ if (!row) return { text: accountText('signedOut', {}, p), tone: 'warn', mode: null };
140
+ if (row.signedIn === false) {
141
+ return { text: accountText(row.apiKey?.error ? 'keyUnreadable' : 'signedOut', {}, p), tone: 'warn', mode: null };
142
+ }
143
+ if (row.mode === 'apiKey') return { text: accountText('apiKeyBadge', {}, p), tone: 'ok', mode: 'apiKey' };
144
+ if (row.mode === 'subscription' && row.signedIn === true) return { text: accountText('subscriptionBadge', {}, p), tone: 'ok', mode: 'subscription' };
145
+ return { text: accountText('checking', {}, p), tone: 'muted', mode: row.mode || null };
146
+ }
147
+
148
+ function planName(plan) {
149
+ const value = String(plan || '').trim();
150
+ return value ? value.charAt(0).toUpperCase() + value.slice(1) : '';
151
+ }
152
+
153
+ /** Who the account is: an email (with its plan), a masked key ("…abcd"), or ''. Never a key. */
154
+ export function accountIdentity(row) {
155
+ if (!row || row.signedIn === false) return '';
156
+ if (row.mode === 'apiKey') return row.apiKey?.hint || row.identity || '';
157
+ const email = row.subscription?.email || row.identity || '';
158
+ const plan = row.subscription?.plan ? planName(row.subscription.plan) : '';
159
+ return [email, plan].filter(Boolean).join(' · ');
160
+ }
161
+
162
+ /** The account's name: the one it was given, Default's in the page's language. */
163
+ export function accountName(row, provider = null) {
164
+ if (!row) return '';
165
+ if (row.isDefault || row.id === 'default') return accountText('defaultName', {}, rowProvider(row, provider));
166
+ return row.label || row.id;
167
+ }
168
+
169
+ /** One line for a menu or a tooltip: "Work · API key · …abcd". */
170
+ export function accountSummary(row, provider = null) {
171
+ if (!row) return '';
172
+ return [accountName(row, provider), accountBadge(row, provider).text, accountIdentity(row)].filter(Boolean).join(' · ');
173
+ }
174
+
175
+ /** A compact credential label where the full account summary is available in a tooltip. */
176
+ export function accountCompactMode(row, provider = null) {
177
+ const p = rowProvider(row, provider);
178
+ if (row?.signedIn !== false && row?.mode === 'apiKey') return accountText('apiShort', {}, p);
179
+ if (row?.signedIn === true && row?.mode === 'subscription') return providerWords(p).service;
180
+ return accountBadge(row, p).text;
181
+ }
182
+
183
+ /** Whether a session can be started under the account now (false only when it is known that it cannot). */
184
+ export function accountUsable(row) {
185
+ return !!row && row.signedIn !== false;
186
+ }
187
+
188
+ /** The two modes of an account that holds both credentials, for a switch; [] otherwise. */
189
+ export function modeChoices(row, provider = null) {
190
+ if (!row?.switchable) return [];
191
+ const p = rowProvider(row, provider);
192
+ return [
193
+ { mode: 'subscription', label: accountText('subscriptionBadge', {}, p), detail: row.subscription?.email || '', current: row.mode === 'subscription' },
194
+ { mode: 'apiKey', label: accountText('apiKeyBadge', {}, p), detail: row.apiKey?.hint || '', current: row.mode === 'apiKey' },
195
+ ];
196
+ }
197
+
198
+ /**
199
+ * The credentials of an account, each with what can be done about it:
200
+ * [{ kind: 'subscription' | 'apiKey', name, present, text, action: 'signIn' | 'signOut' | 'addKey' | 'removeKey', actionText, inUse }].
201
+ */
202
+ export function credentialRows(row, provider = null) {
203
+ if (!row) return [];
204
+ const p = rowProvider(row, provider);
205
+ const sub = row.subscription || {};
206
+ const key = row.apiKey || {};
207
+ const subText = sub.signedIn === true
208
+ ? [sub.email || accountText('signedIn', {}, p), sub.plan ? planName(sub.plan) : ''].filter(Boolean).join(' · ')
209
+ : accountText(sub.signedIn === false ? 'notSignedIn' : 'notChecked', {}, p);
210
+ let keyText = key.present ? (key.hint || accountText('savedKey', {}, p)) : key.error ? accountText('keyUnreadableLong', {}, p) : accountText('noKey', {}, p);
211
+ // A key Codex itself holds (an older `codex login --with-api-key`): said, so its sign-out is understood.
212
+ if (key.present && key.source === 'codex') keyText = accountText('keyKeptBy', { hint: key.hint || accountText('savedKey', {}, p) }, p);
213
+ const subAction = sub.signedIn === true ? 'signOut' : 'signIn';
214
+ const keyAction = key.present || key.error ? 'removeKey' : 'addKey';
215
+ const actionText = { signIn: accountText('signIn', {}, p), signOut: accountText('signOut', {}, p), addKey: accountText('useApiKey', {}, p), removeKey: accountText('removeKey', {}, p) };
216
+ return [
217
+ { kind: 'subscription', name: accountText('credSubscription', {}, p), present: sub.signedIn === true, text: subText, action: subAction, actionText: actionText[subAction], inUse: row.mode === 'subscription' && row.signedIn === true },
218
+ { kind: 'apiKey', name: accountText('credApiKey', {}, p), present: !!key.present, text: keyText, action: keyAction, actionText: actionText[keyAction], inUse: row.mode === 'apiKey' },
219
+ ];
220
+ }
221
+
222
+ /**
223
+ * What picking an account in a tab does. A conversation belongs to the account
224
+ * it started under (its transcript lives in that account's folder), so a tab
225
+ * that has one opens the new account in a new chat.
226
+ * → { effect: 'same' | 'now' | 'next-chat' | 'missing', message }
227
+ */
228
+ export function accountSwitchPlan({ rows = [], currentId = '', targetId = '', hasConversation = false, provider = null } = {}) {
229
+ const current = rows.find((row) => row.id === serverAccountId(currentId));
230
+ const target = rows.find((row) => row.id === serverAccountId(targetId));
231
+ const p = rowProvider(target || current || rows[0], provider);
232
+ if (!target) return { effect: 'missing', message: accountText('missingNote', {}, p) };
233
+ const label = accountName(target, p);
234
+ if (serverAccountId(currentId) === target.id) return { effect: 'same', message: accountText('sameNote', { label }, p) };
235
+ if (hasConversation) return { effect: 'next-chat', message: accountText('nextNote', { label, current: current ? accountName(current, p) : accountText('defaultName', {}, p) }, p) };
236
+ return { effect: 'now', message: accountText('nowNote', { label }, p) };
237
+ }
238
+
239
+ /** The account menu's rows: every account, the current one marked, a signed-out one still choosable (it shows its sign-in). */
240
+ export function accountMenuRows(rows = [], currentId = '', nextId = null, provider = null) {
241
+ return rows.map((row) => {
242
+ const badge = accountBadge(row, provider);
243
+ return {
244
+ id: row.id,
245
+ label: accountName(row, provider),
246
+ badge: badge.text,
247
+ tone: badge.tone,
248
+ identity: accountIdentity(row),
249
+ current: row.id === serverAccountId(currentId),
250
+ next: nextId !== null && row.id === serverAccountId(nextId) && row.id !== serverAccountId(currentId),
251
+ usable: accountUsable(row),
252
+ isDefault: !!row.isDefault || row.id === 'default',
253
+ };
254
+ });
255
+ }
256
+
257
+ /** The menu's trigger: the tab's account and what it runs on. A pending next-chat account is shown after an arrow. */
258
+ export function accountChip(rows = [], currentId = '', nextId = null, provider = null) {
259
+ const current = rows.find((row) => row.id === serverAccountId(currentId));
260
+ const next = nextId !== null ? rows.find((row) => row.id === serverAccountId(nextId)) : null;
261
+ const p = rowProvider(current || rows[0], provider);
262
+ const name = current ? accountName(current, p) : (serverAccountId(currentId) === 'default' ? accountText('defaultName', {}, p) : serverAccountId(currentId));
263
+ const badge = current ? accountBadge(current, p) : { text: '', tone: 'muted' };
264
+ const text = next && next.id !== current?.id ? `${name} → ${accountName(next, p)}` : name;
265
+ return { text, badge: badge.text, tone: badge.tone, tooltip: [current ? accountSummary(current, p) : name, next && next.id !== current?.id ? accountText('nextNote', { label: accountName(next, p), current: name }, p) : ''].filter(Boolean).join('\n') };
266
+ }
267
+
268
+ /**
269
+ * The header account menu, as one model both panels render (account-menu.js):
270
+ * { title, trigger: { name, badge, tone, label }, note, rows }. `hasConversation`:
271
+ * the tab has a conversation, so another account opens in a new chat.
272
+ */
273
+ export function accountMenuModel({ provider = 'claude', rows = [], currentId = '', hasConversation = false } = {}) {
274
+ const p = provider === 'codex' ? 'codex' : 'claude';
275
+ const chip = accountChip(rows, currentId, null, p);
276
+ const current = rows.find((row) => row.id === serverAccountId(currentId));
277
+ const summary = current ? accountSummary(current, p) : chip.text;
278
+ // The header has little room: the trigger's badge is the short form (the service's name for a
279
+ // subscription); the full words are its label and tooltip.
280
+ const short = current?.mode === 'subscription' && current.signedIn === true ? providerWords(p).service : chip.badge;
281
+ return {
282
+ title: accountText('menuTitle', {}, p),
283
+ trigger: { name: chip.text, badge: chip.badge, short, tone: chip.tone, label: accountText('triggerLabel', { summary }, p) },
284
+ note: hasConversation ? accountText('menuNoteChat', { label: chip.text }, p) : accountText('menuNoteNow', {}, p),
285
+ rows: accountMenuRows(rows, currentId, null, p),
286
+ };
287
+ }
288
+
289
+ /**
290
+ * One account's management view: its name, what it runs on, the mode switch
291
+ * (both credentials only), each credential with its action, rename and remove
292
+ * (never for Default).
293
+ */
294
+ export function accountManageModel(row, provider = null) {
295
+ if (!row) return null;
296
+ const p = rowProvider(row, provider);
297
+ const badge = accountBadge(row, p);
298
+ return {
299
+ id: row.id,
300
+ title: accountName(row, p),
301
+ badge: badge.text,
302
+ tone: badge.tone,
303
+ identity: accountIdentity(row),
304
+ usesLabel: accountText('usesGroup', { label: accountName(row, p) }, p),
305
+ modes: modeChoices(row, p),
306
+ credentials: credentialRows(row, p),
307
+ canRename: !(row.isDefault || row.id === 'default'),
308
+ canRemove: !(row.isDefault || row.id === 'default'),
309
+ };
310
+ }
311
+
312
+ const API_KEY_ERROR_KEYS = Object.freeze({
313
+ invalid_key: 'errInvalidKey', oauth_token: 'errOauthToken', key_rejected: 'errKeyRejected', permission_denied: 'errPermission',
314
+ account_unavailable: 'errAccountUnavailable', account_busy: 'errAccountBusy', verification_unreachable: 'errUnreachable',
315
+ verification_unavailable: 'errUnavailable', server_unreachable: 'errServer', storage_failed: 'errStorage',
316
+ });
317
+ /** The sentence for an API-key save error code: only local, allowlisted words, never a provider's error text. */
318
+ export function apiKeyErrorText(provider, code) {
319
+ // An untrusted code never selects an inherited property ("constructor", "__proto__").
320
+ const key = typeof code === 'string' && Object.hasOwn(API_KEY_ERROR_KEYS, code) ? API_KEY_ERROR_KEYS[code] : 'errStorage';
321
+ return accountText(key, {}, provider === 'codex' ? 'codex' : 'claude');
322
+ }
323
+
324
+ // The Claude-only names the first callers imported (claude-account-view.js re-exports them).
325
+ export const CLAUDE_ACCOUNT_COPY = Object.freeze({
326
+ subscription: 'Claude account',
327
+ apiKey: ACCOUNT_COPY.apiKeyBadge,
328
+ signedOut: ACCOUNT_COPY.signedOut,
329
+ checking: ACCOUNT_COPY.checking,
330
+ keyUnreadable: ACCOUNT_COPY.keyUnreadable,
331
+ defaultName: ACCOUNT_COPY.defaultName,
332
+ current: ACCOUNT_COPY.current,
333
+ signInClaude: 'Sign in with Claude',
334
+ useApiKey: ACCOUNT_COPY.useApiKey,
335
+ });
@@ -301,21 +301,27 @@ export async function fetchCodexModels(force = false, accountId = 'default') {
301
301
 
302
302
  let _codexAccountsCache = null;
303
303
  let _codexAccountsLoading = null;
304
+ let _codexWorkerAccountId = null;
305
+ const CODEX_DEFAULT_ACCOUNT = { id: 'default', label: 'Default', isDefault: true, provider: 'codex' };
304
306
 
305
- // Fetch the saved ChatGPT/Codex accounts for launch-account selectors.
306
- // Each account maps to a CODEX_HOME on the server; 'default' is the existing ~/.codex.
307
+ // Codex accounts (per-account CODEX_HOME) for the brain picker, the accounts
308
+ // manager and the launch-account selectors. Each row carries its state
309
+ // (lib/codex-accounts.js describe(): mode, signedIn, identity); the assistant's
310
+ // answer also names the account Codex workers use. 'default' is the ambient ~/.codex.
307
311
  export async function fetchCodexAccounts(force = false) {
308
312
  if (!force && _codexAccountsCache && _codexAccountsCache.length) return _codexAccountsCache;
309
313
  if (_codexAccountsLoading) return _codexAccountsLoading;
310
314
  _codexAccountsLoading = (async () => {
311
315
  try {
312
- const resp = await fetch('/api/codex/accounts');
316
+ const resp = await fetch('/api/assistant/codex/accounts', { cache: 'no-store' }).then((r) => (r.ok ? r : fetch('/api/codex/accounts', { cache: 'no-store' })));
313
317
  const data = await resp.json().catch(() => ({}));
314
318
  const accounts = Array.isArray(data?.accounts) ? data.accounts : [];
315
- _codexAccountsCache = accounts.length ? accounts : [{ id: 'default', label: 'Default', isDefault: true }];
319
+ _codexWorkerAccountId = typeof data?.workerAccountId === 'string' && data.workerAccountId ? data.workerAccountId : null;
320
+ const withDefault = accounts.some((a) => a?.id === 'default') ? accounts : [CODEX_DEFAULT_ACCOUNT, ...accounts];
321
+ _codexAccountsCache = withDefault.map((a) => (a.id === 'default' ? { ...CODEX_DEFAULT_ACCOUNT, ...a, isDefault: true } : a));
316
322
  return _codexAccountsCache;
317
323
  } catch {
318
- _codexAccountsCache = [{ id: 'default', label: 'Default', isDefault: true }];
324
+ _codexAccountsCache = [CODEX_DEFAULT_ACCOUNT];
319
325
  return _codexAccountsCache;
320
326
  } finally {
321
327
  _codexAccountsLoading = null;
@@ -325,23 +331,32 @@ export async function fetchCodexAccounts(force = false) {
325
331
  }
326
332
 
327
333
  export function getCachedCodexAccounts() {
328
- return _codexAccountsCache || [{ id: 'default', label: 'Default', isDefault: true }];
334
+ return _codexAccountsCache || [CODEX_DEFAULT_ACCOUNT];
335
+ }
336
+
337
+ /** The Codex account dispatched Codex workers use when a dispatch names none ('default' when unset). */
338
+ export function getCodexWorkerAccountId() {
339
+ return _codexWorkerAccountId || 'default';
329
340
  }
330
341
 
331
342
  const CLAUDE_DEFAULT_ACCOUNT = { id: 'default', label: 'Default', isDefault: true };
332
343
  let _claudeAccountsCache = null;
333
344
  let _claudeAccountsLoading = null;
345
+ let _claudeWorkerAccountId = null;
334
346
 
335
347
  // Claude account profiles (per-account CLAUDE_CONFIG_DIR) — served by the
336
- // assistant API. 'default' is the ambient ~/.claude and always exists.
348
+ // assistant API. 'default' is the ambient ~/.claude and always exists. Each
349
+ // row carries its state (lib/claude-accounts.js describe(): mode, signedIn,
350
+ // identity); the answer also names the account Claude Code workers use.
337
351
  export async function fetchClaudeAccounts(force = false) {
338
352
  if (!force && _claudeAccountsCache && _claudeAccountsCache.length) return _claudeAccountsCache;
339
353
  if (_claudeAccountsLoading) return _claudeAccountsLoading;
340
354
  _claudeAccountsLoading = (async () => {
341
355
  try {
342
- const resp = await fetch('/api/assistant/claude/accounts');
356
+ const resp = await fetch('/api/assistant/claude/accounts', { cache: 'no-store' });
343
357
  const data = await resp.json().catch(() => ({}));
344
358
  const accounts = Array.isArray(data?.accounts) ? data.accounts : [];
359
+ _claudeWorkerAccountId = typeof data?.workerAccountId === 'string' && data.workerAccountId ? data.workerAccountId : null;
345
360
  const withDefault = accounts.some(a => a?.id === 'default') ? accounts : [CLAUDE_DEFAULT_ACCOUNT, ...accounts];
346
361
  _claudeAccountsCache = withDefault.map(a => (a.id === 'default' ? { ...CLAUDE_DEFAULT_ACCOUNT, ...a, isDefault: true } : a));
347
362
  return _claudeAccountsCache;
@@ -359,6 +374,11 @@ export function getCachedClaudeAccounts() {
359
374
  return _claudeAccountsCache || [CLAUDE_DEFAULT_ACCOUNT];
360
375
  }
361
376
 
377
+ /** The Claude account dispatched Claude Code workers use when a dispatch names none ('default' when unset). */
378
+ export function getClaudeWorkerAccountId() {
379
+ return _claudeWorkerAccountId || 'default';
380
+ }
381
+
362
382
  let _opencodeAgentsCache = null;
363
383
  let _opencodeAgentsLoading = null;
364
384