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
@@ -22,6 +22,8 @@ import { catalogModelRow, fmtModelPrice, hiddenModelFor, ICON_CARET, invalidateM
22
22
  // The Routes editor's model lists (where they come from, how they stay current): asst-route-models.js.
23
23
  import { closeMenu, isMenuOpen, openMenu } from './asst-menu.js';
24
24
  import { decorateControlHead } from './asst-control.js';
25
+ import { accountSummary } from '../account-view.js';
26
+ import { accountUsageText, autoAccountLabel } from '../account-usage.js';
25
27
  import {
26
28
  DEFAULT_ROUTE_MODE,
27
29
  fmtConfidence,
@@ -39,6 +41,18 @@ function obj(v) { return v && typeof v === 'object' && !Array.isArray(v) ? v : n
39
41
  function arr(v) { return Array.isArray(v) ? v : []; }
40
42
  function firstStr(...values) { for (const v of values) { const s = str(v); if (s) return s; } return ''; }
41
43
 
44
+ export function routeDisplayAccount(target, t = null) {
45
+ if (target?.accountId === 'auto') return autoAccountLabel(t);
46
+ if (target?.account) return [accountSummary(target.account, target.provider === 'codex' ? 'codex' : 'claude'), accountUsageText(target.account, t)].join(' · ');
47
+ return target?.accountId ? tf(t)('assistant.routes.account.removed', '{id} · account removed', { id: target.accountId }) : '';
48
+ }
49
+ function routeAccountError(error, t) {
50
+ if (error?.code === 'ACCOUNT_NO_SUBSCRIPTION') return tf(t)('assistant.accountUsage.noSubscription', 'Auto requires a signed-in subscription account.');
51
+ return error?.code === 'ACCOUNT_NOT_FOUND'
52
+ ? tf(t)('assistant.routes.account.removedHint', 'Account removed. Save another account choice before running this route.')
53
+ : tf(t)('assistant.routes.account.signedOut', 'Signed out. Sign in from the provider account menu before running this route.');
54
+ }
55
+
42
56
  /** Local translator: hooks.t when it resolves, else the fallback with {param} interpolation. */
43
57
  function tf(t) {
44
58
  return (key, fallback, params) => {
@@ -93,6 +107,7 @@ function normalizeRouteOption(o) {
93
107
  provider: normalizeProvider(src.provider, str(src.provider) || null),
94
108
  model: str(src.model),
95
109
  effort: str(src.effort) || null,
110
+ ...(Object.hasOwn(src, 'accountId') ? { accountId: src.accountId, account: obj(src.account), accountError: obj(src.accountError) } : {}),
96
111
  label: str(src.label),
97
112
  badge: str(src.badge) || null,
98
113
  tier: str(src.tier) || null,
@@ -101,6 +116,7 @@ function normalizeRouteOption(o) {
101
116
  contextWindow: num(src.contextWindow),
102
117
  reason: str(src.reason),
103
118
  warning: str(src.warning) || null,
119
+ budget: str(src.budget) || null,
104
120
  disabled: src.disabled === true,
105
121
  disabledReason: str(src.disabledReason) || null,
106
122
  };
@@ -174,7 +190,7 @@ export function sameRouteTarget(a, b) {
174
190
  return (str(a.kind) || 'direct') === (str(b.kind) || 'direct')
175
191
  && str(a.provider) === str(b.provider)
176
192
  && modelKey(a.model) === modelKey(b.model)
177
- && (str(a.effort) || '') === (str(b.effort) || '');
193
+ && (str(a.effort) || '') === (str(b.effort) || '') && str(a.accountId) === str(b.accountId);
178
194
  }
179
195
 
180
196
  const BADGE_RANK = { suggested: 0, remembered: 1, current: 2, capable: 3, cheaper: 4, stronger: 5 };
@@ -269,6 +285,7 @@ export function routeOptions(n, { brainLabel = '', t = null } = {}) {
269
285
  // "here" is the brain's own model: prefer its display label over a raw id.
270
286
  title: isHere ? tr('assistant.routes.doHere', 'Do it here with {model}', { model: str(o.label) || hereModel }) : (label || o.id),
271
287
  badgeText: badgeText(o, n, tr),
288
+ disabledReason: o.budget === 'share_used_up' ? routeBudgetReason({ provider: o.provider }, t) : o.disabledReason,
272
289
  selected: o.id === defaultId,
273
290
  };
274
291
  });
@@ -350,6 +367,16 @@ export function routeCatalogRow(pref, catalog) {
350
367
  return pref.model ? catalogModelRow(pref.provider, pref.model, catalog) || aliasRow(pref.provider, pref.model, rows) : rows.find(m => m.isDefault) || null;
351
368
  }
352
369
 
370
+ /** Budget messages are UI copy; never show the server's English disabledReason. */
371
+ export function routeBudgetReason({ provider, dry } = {}, t) {
372
+ const tr = tf(t), names = { 'claude-code': 'Claude Code', codex: 'Codex', opencode: 'OpenCode' };
373
+ if (provider) return tr('assistant.routes.budgetProvider', "Budget: the {provider} share of this session's budget is used up.", { provider: names[provider] || provider });
374
+ const list = [...new Set(arr(dry))].map(id => names[id] || id);
375
+ if (!list.length) return tr('assistant.routes.budgetSpent', "Budget: this session's budget is spent.");
376
+ return tr(list.length === 1 ? 'assistant.routes.budgetDryOne' : 'assistant.routes.budgetDryMany',
377
+ list.length === 1 ? 'Budget: the {list} share is used up.' : 'Budget: the {list} shares are used up.', { list: list.join(', ') });
378
+ }
379
+
353
380
  /** Text parts of a route line (pure). */
354
381
  export function routeLineParts(route, phase = 'decided', { brainLabel = '', t = null } = {}) {
355
382
  const tr = tf(t);
@@ -366,10 +393,12 @@ export function routeLineParts(route, phase = 'decided', { brainLabel = '', t =
366
393
  else if (phase === 'declined' && str(r.reasonCode) === 'no_capable_model') {
367
394
  state = str(r.needs) === 'vision' ? tr('assistant.routes.reason.noSeeingModel', 'no model can see images') : tr('assistant.routes.reason.noCapableModel', 'no model can make this');
368
395
  }
396
+ else if (str(r.reasonCode) === 'budget_exhausted') state = routeBudgetReason({ dry: r.dry }, t);
369
397
  else if (phase === 'declined') state = tr('assistant.routes.state.declined', 'declined');
370
398
  else if (phase === 'cancelled') state = tr('assistant.routes.state.cancelled', 'cancelled');
371
399
  let decided = '';
372
- switch (str(r.decidedBy)) {
400
+ switch (r.source === 'budget' ? 'budget' : str(r.decidedBy)) {
401
+ case 'budget': decided = tr('assistant.routes.budgetDecision', 'budget re-route'); break;
373
402
  case 'brain': decided = tr('assistant.routes.decidedBy', 'decided by {model}', { model: brainLabel || tr('assistant.routes.theBrainLower', 'the assistant') }); break;
374
403
  case 'user': decided = tr('assistant.routes.youChose', 'you chose'); break;
375
404
  case 'remembered': decided = tr('assistant.routes.remembered', 'your saved choice'); break;
@@ -377,6 +406,8 @@ export function routeLineParts(route, phase = 'decided', { brainLabel = '', t =
377
406
  default: decided = r.remembered ? tr('assistant.routes.remembered', 'your saved choice') : '';
378
407
  }
379
408
  const meta = [];
409
+ const accountLabel = routeDisplayAccount(target, t);
410
+ if (accountLabel) meta.push(accountLabel);
380
411
  if (effort && effort !== 'off') meta.push(effort);
381
412
  if (confidence) meta.push(confidence);
382
413
  return {
@@ -390,7 +421,7 @@ export function routeLineParts(route, phase = 'decided', { brainLabel = '', t =
390
421
  muted: phase === 'expired' || phase === 'declined' || phase === 'cancelled',
391
422
  pending: phase === 'pending' || phase === 'card',
392
423
  decidedText: decided,
393
- reason: str(r.reason),
424
+ reason: target?.accountError ? routeAccountError(target.accountError, t) : str(r.reason),
394
425
  alternatives: arr(r.alternatives).map(a => routeTargetLabel(a, { effort: true })).filter(Boolean),
395
426
  corrections: arr(r.corrections).map(c => correctionText(c, tr)).filter(Boolean),
396
427
  };
@@ -400,9 +431,15 @@ export function routeLineParts(route, phase = 'decided', { brainLabel = '', t =
400
431
  function correctionText(c, tr) {
401
432
  if (typeof c === 'string') return c;
402
433
  if (!c || typeof c !== 'object') return '';
434
+ if (['account_auto', 'account_limited', 'accounts_all_limited'].includes(c.reason) && c.usage) {
435
+ const key = c.reason === 'account_auto' ? 'autoPicked' : c.reason === 'account_limited' ? 'fallbackPicked' : 'allLimitedPicked';
436
+ const fallbacks = { autoPicked: 'Auto chose {to}: {usage}', fallbackPicked: '{from} out of usage → {to}: {usage}', allLimitedPicked: 'All subscriptions limited; soonest reset → {to}: {usage}' };
437
+ const text = tr(`assistant.accountUsage.${key}`, fallbacks[key], { from: str(c.from), to: str(c.to), usage: accountUsageText({ mode: 'subscription', usage: c.usage }, (key, params) => tr(key, key, params)) });
438
+ return text + (c.workerForAccount ? ' · ' + tr('assistant.accountUsage.directWorkerHint', 'Runs on a worker because this conversation belongs to another account.') : '');
439
+ }
403
440
  if (c.reason === 'remembered_route') return tr('assistant.routes.correction.remembered', 'your saved route {to} replaced {from}', { to: str(c.to), from: str(c.from) });
404
441
  if (c.reason === 'remembered_disabled') return tr('assistant.routes.correction.rememberedDisabled', 'saved route {from} is disabled, kept the pick', { from: str(c.from) });
405
- const text = firstStr(c.text, c.summary);
442
+ const text = firstStr(c.text, c.summary, c.message);
406
443
  if (text) return text;
407
444
  if (c.field && (c.from || c.to)) return `${c.field}: ${str(c.from) || '—'} → ${str(c.to) || 'default'}`;
408
445
  return str(c.field);
@@ -416,6 +453,14 @@ export function routeLineText(route, phase, opts) {
416
453
  return [`⇄ ${p.taskClass} → ${head}`, ...p.meta].join(' · ');
417
454
  }
418
455
 
456
+ /** Everything the one-line row can cut short (meta, state included): its tooltip. */
457
+ export function routeLineTitle(route, phase, opts) {
458
+ const p = routeLineParts(route, phase, opts);
459
+ const tail = [p.kindLabel, p.targetLabel, ...p.meta, p.stateText].filter(Boolean).join(' · ');
460
+ if (!tail) return `⇄ ${p.taskClass}`;
461
+ return `⇄ ${p.taskClass} ${p.kindLabel || p.targetLabel ? '→' : '·'} ${tail}`;
462
+ }
463
+
419
464
  // ═══════════════════════════════════════════
420
465
  // DOM
421
466
  // ═══════════════════════════════════════════
@@ -449,8 +494,12 @@ function providerColor(provider) {
449
494
  let _lineUid = 0;
450
495
 
451
496
  /**
452
- * The compact route line. opts: { phase, t, providerIcon, brainLabel() , onChangeRoutes() }
453
- * Returns { el, routeId, update(route, phase) }.
497
+ * The compact route line, always one line: the glyph, arrow, kind and mark
498
+ * hold; the rest (model · account · usage, or the state) gives way first,
499
+ * then the class; the title has it all. data-live: "waiting" while it waits
500
+ * for a choice, "working" while a run it dispatched is (setRunning).
501
+ * opts: { phase, t, providerIcon, brainLabel() , onChangeRoutes() }
502
+ * Returns { el, routeId, update(route, phase), setRunning(on) }.
454
503
  */
455
504
  export function createRouteLine(route, opts = {}) {
456
505
  const tr = tf(opts.t);
@@ -458,20 +507,19 @@ export function createRouteLine(route, opts = {}) {
458
507
  const detailId = `asst-route-detail-${++_lineUid}`;
459
508
  node.innerHTML = `
460
509
  <button type="button" class="asst-route-line-head" aria-expanded="false" aria-controls="${detailId}">
461
- <span class="asst-route-glyph" aria-hidden="true">⇄</span>
510
+ <span class="asst-route-glyph asst-icon" aria-hidden="true">${ICON_ROUTE}</span>
462
511
  <span class="asst-route-class"></span>
463
512
  <span class="asst-route-arrow" aria-hidden="true">→</span>
464
513
  <span class="asst-route-kind"></span>
465
514
  <span class="asst-route-icon asst-icon" aria-hidden="true"></span>
466
- <span class="asst-route-target"></span>
467
- <span class="asst-route-meta"></span>
468
- <span class="asst-route-state"></span>
515
+ <span class="asst-route-rest"><span class="asst-route-target"></span> <span class="asst-route-meta"></span> <span class="asst-route-state"></span></span>
469
516
  <span class="asst-route-chev" aria-hidden="true">&#x203A;</span>
517
+ <span class="asst-route-sweep" aria-hidden="true"></span>
470
518
  </button>
471
519
  <div class="asst-route-detail" id="${detailId}" hidden></div>`;
472
520
  const head = node.querySelector('.asst-route-line-head');
473
521
  const detail = node.querySelector('.asst-route-detail');
474
- let current = { route: obj(route) || {}, phase: opts.phase || 'decided' };
522
+ let current = { route: obj(route) || {}, phase: opts.phase || 'decided', running: false };
475
523
 
476
524
  head.addEventListener('click', () => {
477
525
  const open = head.getAttribute('aria-expanded') !== 'true';
@@ -487,6 +535,8 @@ export function createRouteLine(route, opts = {}) {
487
535
  node.dataset.phase = current.phase;
488
536
  node.classList.toggle('muted', p.muted);
489
537
  node.classList.toggle('pending', p.pending);
538
+ const live = p.pending ? 'waiting' : current.running && !p.muted && !p.stateText ? 'working' : '';
539
+ if (live) node.dataset.live = live; else delete node.dataset.live;
490
540
  node.querySelector('.asst-route-class').textContent = p.taskClass;
491
541
  const arrow = node.querySelector('.asst-route-arrow');
492
542
  const kindEl = node.querySelector('.asst-route-kind');
@@ -494,7 +544,7 @@ export function createRouteLine(route, opts = {}) {
494
544
  const targetEl = node.querySelector('.asst-route-target');
495
545
  const metaEl = node.querySelector('.asst-route-meta');
496
546
  const stateEl = node.querySelector('.asst-route-state');
497
- arrow.hidden = !p.targetLabel && !p.pending;
547
+ arrow.hidden = !p.targetLabel && !p.pending && !p.kindLabel;
498
548
  kindEl.textContent = p.kindLabel;
499
549
  kindEl.hidden = !p.kindLabel;
500
550
  iconEl.innerHTML = p.targetLabel ? providerIconHtml(p.provider, opts) : '';
@@ -506,7 +556,9 @@ export function createRouteLine(route, opts = {}) {
506
556
  metaEl.hidden = !p.meta.length;
507
557
  stateEl.textContent = p.stateText;
508
558
  stateEl.hidden = !p.stateText;
559
+ node.querySelector('.asst-route-rest').hidden = targetEl.hidden && metaEl.hidden && stateEl.hidden;
509
560
  head.setAttribute('aria-label', routeLineText(current.route, current.phase, { brainLabel, t: opts.t }));
561
+ head.title = routeLineTitle(current.route, current.phase, { brainLabel, t: opts.t });
510
562
 
511
563
  detail.innerHTML = '';
512
564
  const rows = [];
@@ -535,6 +587,12 @@ export function createRouteLine(route, opts = {}) {
535
587
  if (phase) current.phase = phase;
536
588
  render();
537
589
  },
590
+ /** A run this route dispatched is queued or working (the renderer knows from its run cards). */
591
+ setRunning(on) {
592
+ if (current.running === !!on) return;
593
+ current.running = !!on;
594
+ render();
595
+ },
538
596
  };
539
597
  }
540
598
 
@@ -705,7 +763,7 @@ export function renderRouteCard(container, input, hooks = {}) {
705
763
  row.setAttribute('aria-disabled', o.disabled ? 'true' : 'false');
706
764
  row.tabIndex = -1;
707
765
  const kindWord = o.kind && o.kind !== 'direct' ? routeKindLabel(o.kind) : '';
708
- const sub = [kindWord, o.effort && o.effort !== 'off' ? o.effort : '', o.price ? priceText(o.price) : '', o.vision ? tr('assistant.brain.vision', 'vision') : '', o.reason].filter(Boolean).join(' · ');
766
+ const sub = [kindWord, routeDisplayAccount(o, hooks.t), o.effort && o.effort !== 'off' ? o.effort : '', o.price ? priceText(o.price) : '', o.vision ? tr('assistant.brain.vision', 'vision') : '', o.reason].filter(Boolean).join(' · ');
709
767
  row.innerHTML = `
710
768
  <span class="asst-route-num" aria-hidden="true">${esc(o.number)}</span>
711
769
  <span class="asst-route-opt-icon asst-icon" aria-hidden="true"${o.provider ? ` style="color:${esc(providerColor(o.provider))}"` : ''}>${providerIconHtml(o.provider, hooks)}</span>
@@ -718,7 +776,7 @@ export function renderRouteCard(container, input, hooks = {}) {
718
776
  row.querySelector('.asst-route-opt-title').textContent = o.title;
719
777
  if (sub) row.querySelector('.asst-route-opt-sub').textContent = sub;
720
778
  const warn = row.querySelector('.asst-route-opt-warn');
721
- if (warn) warn.textContent = o.disabled && o.disabledReason ? o.disabledReason : o.warning;
779
+ if (warn) warn.textContent = o.accountError ? routeAccountError(o.accountError, hooks.t) : o.disabled && o.disabledReason ? o.disabledReason : o.warning;
722
780
  if (o.badgeText) row.querySelector('.asst-route-badge').textContent = o.badgeText;
723
781
  row.addEventListener('click', () => { if (!o.disabled) select(o.id, { focus: true }); });
724
782
  return row;
@@ -1,6 +1,8 @@
1
1
  // Model routes: draft-only editor. All model data comes through asst-route-models.
2
2
  // The global default and saved preferences are written together only on Save.
3
- import { clampEffortId, getEffortLevelsForModel, modelEffortIds } from '../agent-runtime-options.js';
3
+ import { clampEffortId, getEffortLevelsForModel, modelEffortIds, fetchClaudeAccounts, fetchCodexAccounts, getCachedClaudeAccounts, getCachedCodexAccounts, getClaudeWorkerAccountId, getCodexWorkerAccountId } from '../agent-runtime-options.js';
4
+ import { accountSummary, accountName, accountBadge, accountIdentity } from '../account-view.js';
5
+ import { accountUsageText, autoAccountLabel } from '../account-usage.js';
4
6
  import { getProviderMeta } from '../provider-icons.js';
5
7
  import { getRouting, putRouting } from './asst-api.js';
6
8
  import { catalogModelRow, hiddenModelFor, ICON_CARET, openModelMenu } from './asst-brain-picker.js';
@@ -30,7 +32,33 @@ function translator(t) {
30
32
  return typeof text === 'string' && text !== key ? text : String(fallback).replace(/\{(\w+)\}/g, (_, k) => params?.[k] ?? `{${k}}`);
31
33
  };
32
34
  }
33
- function choiceKey(p) { return p ? JSON.stringify([p.kind || 'direct', p.provider || '', p.model || '', p.effort || '']) : ''; }
35
+ function choiceKey(p) { return p ? JSON.stringify([p.kind || 'direct', p.provider || '', p.model || '', p.effort || '', p.accountId || '']) : ''; }
36
+
37
+ export function routeAccountView(pref, task, brain, accounts, workers = {}, t = null) {
38
+ const tr = translator(t);
39
+ const empty = { label: tr('assistant.routes.editorUnusedValue', '—'), readOnly: true, options: [], hint: '' };
40
+ if (!pref || pref.provider === 'opencode') return empty;
41
+ const direct = task.id === 'chat' || routeExecutionKind(pref, task, brain) === 'direct';
42
+ const provider = task.id === 'chat' ? brain.provider : pref.provider;
43
+ if (!['claude-code', 'codex'].includes(provider)) return empty;
44
+ const rows = accounts[provider] || [];
45
+ const providerView = provider === 'codex' ? 'codex' : 'claude';
46
+ const id = task.id === 'chat' ? brain.accountId || 'default' : pref.accountId || (direct ? brain.accountId || 'default' : null);
47
+ const selected = rows.find(a => a.id === id);
48
+ const missing = id && id !== 'auto' && !selected;
49
+ const removed = tr('assistant.routes.account.removed', '{id} · account removed', { id });
50
+ const warning = missing ? tr('assistant.routes.account.removedHint', 'Account removed. Save another account choice before running this route.') : selected?.signedIn === false ? tr('assistant.routes.account.signedOut', 'Signed out. Sign in from the provider account menu before running this route.') : '';
51
+ const worker = rows.find(a => a.id === (workers[provider] || 'default'));
52
+ const label = id === 'auto' ? autoAccountLabel(t) : selected ? accountSummary(selected, providerView) : missing ? removed : tr('assistant.routes.account.workersDefault', 'Workers default');
53
+ const options = [
54
+ { id: '', label: direct ? tr('assistant.routes.editorInConversation', 'This conversation') : tr('assistant.routes.account.workersDefault', 'Workers default'), desc: worker ? accountSummary(worker, providerView) : '', selected: !pref.accountId },
55
+ { id: 'auto', label: autoAccountLabel(t), desc: tr('assistant.accountUsage.autoHint', 'Signed-in subscriptions only. Another account runs on a worker.'), selected: id === 'auto' },
56
+ ...rows.map(a => ({ id: a.id, label: accountName(a, providerView), tag: accountBadge(a, providerView).text, desc: a.signedIn === false ? tr('assistant.routes.account.signedOut', 'Signed out. Sign in from the provider account menu before running this route.') : [accountIdentity(a), accountUsageText(a, t)].filter(Boolean).join(' · '), selected: !!pref.accountId && a.id === id })),
57
+ ];
58
+ return { label, readOnly: task.id === 'chat', options, warning, hint: warning || (selected ? `${label} · ${accountUsageText(selected, t)}` : label) };
59
+ }
60
+
61
+ export function routeModelAccount(pref, provider) { return pref?.provider === provider ? pref.accountId || null : null; }
34
62
 
35
63
  /** Only actual choices change the draft. Catalog label/by updates are not user edits. */
36
64
  export function routeEditorPatch(mode, initialMode, prefs, edits) {
@@ -307,7 +335,7 @@ export async function openRoutesEditor(hooks = {}) {
307
335
  const note = el('div', 'asst-routes-catalog-note'); note.setAttribute('role', 'status'); note.append(el('span', '', tr('assistant.routes.editorState.modelsFailed', 'Could not refresh the model list. Showing provider lists where available. Reload models to verify your choices.'))); table.append(note);
308
336
  } else if (!rowsOf(state.catalog).length) table.append(el('div', 'asst-routes-catalog-note', tr('assistant.routes.editorState.modelsEmpty', 'No models are available. Check Models or connect a provider, then reload.')));
309
337
  const columns = el('div', 'asst-routes-columns'); columns.setAttribute('aria-hidden', 'true');
310
- for (const key of ['task', 'model', 'effort', 'run', 'reset']) columns.append(el('span', '', tr(`assistant.routes.columns.${key}`, key)));
338
+ for (const key of ['task', 'model', 'effort', 'run', 'account', 'reset']) columns.append(el('span', '', tr(`assistant.routes.columns.${key}`, key)));
311
339
  table.append(columns);
312
340
  const list = el('div', 'asst-routes-list');
313
341
  const taskList = tasks();
@@ -363,7 +391,7 @@ export async function openRoutesEditor(hooks = {}) {
363
391
  else if (target?.model) {
364
392
  const levels = modelEffortIds(catalogModelRow(target.provider, target.model, state.catalog));
365
393
  const effort = !pref?.effort ? null : levels ? clampEffortId(pref.effort, levels) : pref.provider === target.provider ? pref.effort : null;
366
- setChoice(task.id, { kind: task.dispatchOnly ? 'dispatch' : pref?.kind || task.defaultKind || 'direct', provider: target.provider, model: target.model, effort, label: target.label || target.model }, modelBtn.dataset.focusKey);
394
+ setChoice(task.id, { kind: task.dispatchOnly ? 'dispatch' : pref?.kind || task.defaultKind || 'direct', provider: target.provider, model: target.model, effort, accountId: routeModelAccount(pref, target.provider), label: target.label || target.model }, modelBtn.dataset.focusKey);
367
395
  }
368
396
  },
369
397
  });
@@ -420,9 +448,20 @@ export async function openRoutesEditor(hooks = {}) {
420
448
  if (task.dispatchOnly) run.title = tr('assistant.routes.workerOnly', 'Worker only');
421
449
  else if (pref && pref.provider !== brain().provider && task.id !== 'chat') run.title = tr('assistant.routes.otherProviderWorker', 'A model from another provider runs on a worker.');
422
450
  const runCell = el('div', 'asst-routes-cell asst-routes-run-cell'); runCell.append(el('span', 'asst-routes-cell-label', tr('assistant.routes.columns.run', 'Runs in')), run); node.append(runCell);
451
+ const account = routeAccountView(pref, task, brain(), { 'claude-code': getCachedClaudeAccounts(), codex: getCachedCodexAccounts() }, { 'claude-code': getClaudeWorkerAccountId(), codex: getCodexWorkerAccountId() }, hooks.t);
452
+ const accountCell = el('div', 'asst-routes-cell asst-routes-account-cell');
453
+ const accountBtn = fieldButton('asst-routes-account', task, tr('assistant.routes.columns.account', 'Account'), account.label, () => {
454
+ state.menuFocus = `${task.id}-account`;
455
+ openMenuFor(accountBtn, tr('assistant.routes.columns.account', 'Account'), account.options.map(o => ({ ...o, kind: 'radio', onSelect: () => setChoice(task.id, { ...pref, accountId: o.id || null }, `${task.id}-account`) })));
456
+ });
457
+ accountBtn.dataset.focusKey = `${task.id}-account`; accountBtn.disabled = account.readOnly; accountBtn.title = account.hint;
458
+ if (!account.readOnly) caret(accountBtn);
459
+ if (account.warning) accountBtn.classList.add('asst-dd-warn');
460
+ accountCell.append(el('span', 'asst-routes-cell-label', tr('assistant.routes.columns.account', 'Account')), accountBtn); node.append(accountCell);
423
461
  const resetCell = el('div', 'asst-routes-reset-cell');
424
462
  if (pref) { const reset = button('asst-iconbtn asst-routes-reset', tr('assistant.routes.reset', 'Reset {taskClass}', { taskClass: task.label }), () => setChoice(task.id, null, `${task.id}-model`)); reset.dataset.focusKey = `${task.id}-reset`; reset.title = reset.getAttribute('aria-label'); reset.innerHTML = ICON_RESET; resetCell.append(reset); }
425
463
  node.append(resetCell);
464
+ if (account.warning) { const note = el('div', 'asst-routes-row-warning', account.warning); note.id = `${nameId}-account-warning`; accountBtn.setAttribute('aria-describedby', note.id); node.append(note); }
426
465
  if (capabilityNote) node.append(capabilityNote);
427
466
  if (issue) { description.hidden = true; node.querySelector('.asst-routes-row-note')?.remove(); const note = el('div', 'asst-routes-row-warning', issueText(issue, task)); note.title = note.textContent; note.id = `${nameId}-issue`; modelBtn.setAttribute('aria-describedby', `${source.id} ${note.id}`); modelBtn.title = `${label} — ${note.textContent}`; node.append(note); }
428
467
  if (task.id === 'chat' || task.id === 'vision') {
@@ -453,7 +492,7 @@ export async function openRoutesEditor(hooks = {}) {
453
492
  const pending = preserve ? changes() : {};
454
493
  state.loading = true; state.error = ''; state.modelsLoading = true; render();
455
494
  try {
456
- const [data, result] = await Promise.all([getRouting(), loadRouteModels().then(catalog => ({ catalog })).catch(() => ({ catalog: state.catalog, error: true }))]);
495
+ const [data, result] = await Promise.all([getRouting(), loadRouteModels().then(catalog => ({ catalog })).catch(() => ({ catalog: state.catalog, error: true })), Promise.allSettled([fetchClaudeAccounts(true), fetchCodexAccounts(true)])]);
457
496
  if (!overlay.isConnected) return;
458
497
  state.data = data; state.version = data.version ?? null; state.initialMode = normalizeRouteMode(data.routing?.defaultMode, DEFAULT_ROUTE_MODE);
459
498
  state.mode = pending.defaultMode || state.initialMode; state.prefs = { ...(data.routing?.preferences || {}) }; state.patch = pending.preferences || {};
@@ -485,7 +524,7 @@ export async function openRoutesEditor(hooks = {}) {
485
524
  import { openMenu } from './asst-menu.js';
486
525
  function openMenuFor(anchor, title, items) {
487
526
  if (currentMenu()) { closeMenu({ restoreFocus: true }); return null; }
488
- const menu = openMenu(anchor, { title, items, className: 'asst-route-field-menu', role: 'menu' });
527
+ const menu = openMenu(anchor, { title, items, className: `asst-route-field-menu${anchor.classList.contains('asst-routes-account') ? ' asst-route-account-menu' : ''}`, role: 'menu' });
489
528
  const overlay = anchor.closest('.asst-routes-overlay'); if (overlay) adoptMenu(menu, overlay); return menu;
490
529
  }
491
530
  function adoptMenu(menu, overlay) {
@@ -15,7 +15,7 @@ export const SLASH_COMMANDS = [
15
15
  { name: 'project', usage: '/project <path>', descKey: 'assistant.slash.project', desc: 'Set the working project' },
16
16
  { name: 'routes', usage: '/routes [always|unsure|never]', descKey: 'assistant.slash.routes', desc: 'Set when to ask before routing (or open the model routes)' },
17
17
  { name: 'models', usage: '/models [list | archived | hide <id…> | show <id…>]', descKey: 'assistant.slash.models', desc: 'Switch models off or on for the Assistant (or open the Models list)' },
18
- { name: 'budget', usage: '/budget', descKey: 'assistant.slash.budget', desc: 'Set the money caps (per run, session, brain)' },
18
+ { name: 'budget', usage: '/budget', descKey: 'assistant.slash.budget', desc: 'Set the session budget and view provider shares' },
19
19
  { name: 'computer', usage: '/computer [on|off]', descKey: 'assistant.slash.computer', desc: 'Let the assistant use your Mac' },
20
20
  { name: 'stop', usage: '/stop [runId|all]', descKey: 'assistant.slash.stop', desc: 'Stop a run, everything, or the current turn' },
21
21
  { name: 'new', usage: '/new [provider]', descKey: 'assistant.slash.new', desc: 'Start a fresh session' },
@@ -181,7 +181,7 @@ export function resolveBrainDefaults(brain, catalog = {}) {
181
181
 
182
182
  if (ACCOUNT_PROVIDERS.has(b.provider)) {
183
183
  const accounts = catalog.accounts?.[b.provider] || [];
184
- if (!b.accountId || (accounts.length && !accounts.some(a => a?.id === b.accountId))) {
184
+ if (!b.accountId || (b.accountId !== 'auto' && accounts.length && !accounts.some(a => a?.id === b.accountId))) {
185
185
  b.accountId = accounts.find(a => a?.isDefault)?.id || accounts[0]?.id || 'default';
186
186
  }
187
187
  } else {
@@ -367,10 +367,10 @@ export function injectAssistantStyles() {
367
367
  .asst-chip.asst-suggestion > .asst-icon svg { width: 20px; height: 14px; }
368
368
  .asst-chip.asst-suggestion > .asst-icon { color: var(--asst-text-3); }
369
369
  .asst-chip.asst-suggestion:is(:hover, :focus-visible) > .asst-icon { color: var(--asst-text); }
370
- /* "Choose a project": the toolbar's Working project field, centred under the lead (asst-brain-picker.js mountEmptyProject). An md capsule like the suggestions. */
371
- .asst-empty-project { display: flex; justify-content: center; max-width: 100%; }
372
- .asst-dd.asst-empty-project-btn { max-width: min(100%, 360px); }
373
- .asst-empty-project-btn .asst-dd-label { max-width: 30ch; }
370
+ /* Project and Budget: shared md capsules under the lead, like the suggestions. */
371
+ .asst-empty-project { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; max-width: 100%; }
372
+ .asst-dd.asst-empty-control-btn { max-width: min(100%, 360px); }
373
+ .asst-empty-control-btn .asst-dd-label { max-width: 30ch; }
374
374
  .asst-empty-project-key { flex-shrink: 0; color: var(--asst-text-3); font-weight: 400; }
375
375
 
376
376
  /* ── Messages ───────────────────────────────────────────────────── */
@@ -578,32 +578,61 @@ export function injectAssistantStyles() {
578
578
  color: var(--asst-text-2);
579
579
  }
580
580
 
581
- /* ── Route lines ────────────────────────────────────────────────── */
581
+ /* ── Route lines ────────────────────────────────────────────────────
582
+ One line at any width, in the cards' column (the rack's 560px, no
583
+ hang): the glyph, arrow, kind and mark hold; the rest (model ·
584
+ account · usage, or the state) takes only the room left (flex-basis 0)
585
+ and ellipsizes first; the class gives way once the rest is down to
586
+ its "…". The head's title carries the whole text. */
582
587
  .asst-route-line { font-size: 12.5px; }
583
588
  .asst-route-line-head {
584
- display: inline-flex;
589
+ position: relative;
590
+ display: flex;
585
591
  align-items: center;
586
592
  gap: 6px;
587
- max-width: calc(100% + 8px);
593
+ width: fit-content;
594
+ max-width: min(100%, 560px);
595
+ min-width: 0;
588
596
  min-height: 26px;
589
- margin-left: -8px;
590
597
  padding: 3px 8px;
598
+ overflow: hidden;
591
599
  border: 0;
592
600
  border-radius: var(--asst-r-ctl);
593
601
  background: transparent;
594
602
  color: var(--asst-text-2);
595
603
  font: inherit;
604
+ white-space: nowrap;
596
605
  text-align: left;
597
606
  cursor: pointer;
598
607
  }
599
608
  .asst-route-line-head:hover { background: var(--asst-fill-1); }
600
609
  .asst-route-glyph { color: var(--asst-brand); }
610
+ .asst-route-glyph svg, .asst-route-icon svg { width: 12px; height: 12px; }
611
+ .asst-route-glyph, .asst-route-arrow, .asst-route-kind, .asst-route-icon, .asst-route-chev { flex: none; }
601
612
  .asst-route-arrow, .asst-route-chev { color: var(--asst-text-4); }
602
613
  .asst-route-kind { color: var(--asst-text-3); font-size: 11.5px; }
603
- .asst-route-icon svg { width: 12px; height: 12px; }
604
- .asst-route-target { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--asst-text); font-weight: 500; }
614
+ .asst-route-class { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
615
+ .asst-route-rest { flex: 1 1 0; min-width: 1.2em; overflow: hidden; text-overflow: ellipsis; color: var(--asst-text-3); }
616
+ .asst-route-target { color: var(--asst-text); font-weight: 500; }
605
617
  .asst-route-meta, .asst-route-state { flex-shrink: 0; color: var(--asst-text-3); white-space: nowrap; }
618
+ .asst-route-meta:not([hidden]) + .asst-route-state::before { content: '· '; color: var(--asst-text-3); }
606
619
  .asst-route-line.pending .asst-route-state { color: var(--asst-warn); }
620
+ /* Live: the rack's sweep (asst-rk-sweep, asst-rack-styles.js) on the row's bottom edge, gold while
621
+ a run it dispatched works (the mark ticks like the rack's running tick), the warn tone while it
622
+ waits for your choice. Settled rows hold still. */
623
+ .asst-route-sweep { position: absolute; left: 0; right: 0; bottom: 0; height: 1.5px; overflow: hidden; pointer-events: none; }
624
+ .asst-route-line[data-live] .asst-route-sweep::after {
625
+ content: '';
626
+ position: absolute;
627
+ top: 0;
628
+ bottom: 0;
629
+ left: 0;
630
+ width: 38%;
631
+ background: linear-gradient(90deg, transparent, var(--asst-brand), transparent);
632
+ animation: asst-rk-sweep 1.8s cubic-bezier(.45, 0, .55, 1) infinite;
633
+ }
634
+ .asst-route-line[data-live="waiting"] .asst-route-sweep::after { background: linear-gradient(90deg, transparent, var(--asst-warn), transparent); }
635
+ .asst-route-line[data-live="working"] .asst-route-icon { animation: asst-rk-tick 1.1s ease-in-out infinite; }
607
636
  .asst-route-line.muted .asst-route-glyph { color: var(--asst-text-4); }
608
637
  .asst-route-line.muted .asst-route-class { color: var(--asst-text-3); }
609
638
  .asst-route-chev { transition: transform var(--asst-dur-base) var(--asst-ease-standard); }
@@ -1079,6 +1108,11 @@ export function injectAssistantStyles() {
1079
1108
  /* Fields (toolbar and composer): glyph · seam · value ▾ */
1080
1109
  .asst-dd { flex: 0 1 auto; justify-content: flex-start; max-width: calc(18ch + 60px); }
1081
1110
  .asst-dd .asst-dd-label { min-width: 0; max-width: 18ch; overflow: hidden; text-overflow: ellipsis; }
1111
+ .asst-dd-model .asst-dd-label.asst-model-account-label { display: flex; gap: 4px; max-width: 34ch; }
1112
+ .asst-model-account-label :is(.asst-model-name, .asst-model-account) { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
1113
+ .asst-model-account-label .asst-model-name { flex: 1 1 auto; }
1114
+ .asst-model-account-label .asst-model-account { flex: 0 1 auto; min-width: 4ch; max-width: 12ch; }
1115
+ .asst-model-account-label .asst-model-mode { flex-shrink: 0; }
1082
1116
  /* A display name ("Sonnet 5", a project) is sans; a raw model id (opus[1m]) is mono. */
1083
1117
  .asst-dd.asst-dd-model[data-raw] .asst-dd-label { font-family: var(--asst-mono); font-weight: 400; }
1084
1118
  .asst-dd-caret {
@@ -1503,11 +1537,50 @@ export function injectAssistantStyles() {
1503
1537
  .asst-account-label { color: var(--asst-text); }
1504
1538
  .asst-account-sub { color: var(--asst-text-3); font-size: 11px; }
1505
1539
  .asst-account-row input.asst-dd-input { margin: 0; }
1540
+ /* Claude accounts (claude-account-view.js): what each runs on, its credentials and their actions. */
1541
+ .asst-account-row.claude { flex-wrap: wrap; align-items: flex-start; }
1542
+ .asst-account-row.claude .asst-account-main { flex-basis: 60%; }
1543
+ .asst-account-head { display: flex; align-items: center; gap: 6px; min-width: 0; }
1544
+ .asst-account-badge { flex-shrink: 0; padding: 1px 6px; border-radius: var(--asst-pill-r-xs); color: var(--asst-text-2); font-size: 10.5px; box-shadow: inset 0 0 0 1px var(--asst-line-2); }
1545
+ .asst-account-badge[data-tone="ok"] { color: var(--asst-text); box-shadow: inset 0 0 0 1px var(--asst-pill-on-ring); }
1546
+ .asst-account-badge[data-tone="warn"] { color: var(--asst-warn); box-shadow: inset 0 0 0 1px var(--asst-pill-warn-ring); }
1547
+ .asst-account-creds { display: flex; flex-direction: column; gap: 2px; margin-top: 4px; color: var(--asst-text-3); font-size: 11px; }
1548
+ .asst-account-cred { display: flex; align-items: center; gap: 6px; }
1549
+ .asst-account-cred-k { min-width: 120px; }
1550
+ .asst-account-actions { display: flex; flex-wrap: wrap; flex-basis: 100%; gap: 6px; margin-top: 6px; }
1551
+ .asst-account-form { display: flex; flex-basis: 100%; gap: 6px; margin-top: 6px; }
1552
+ .asst-account-form input { flex: 1; min-width: 0; }
1553
+ .asst-account-msg { flex-basis: 100%; margin-top: 4px; color: var(--asst-text-3); font-size: 11px; }
1554
+ .asst-dd.asst-dd-warn { color: var(--asst-warn); box-shadow: inset 0 0 0 1px var(--asst-pill-warn-ring); }
1506
1555
  .asst-login-box { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; border-radius: var(--asst-r-field); box-shadow: inset 0 0 0 1px var(--asst-line-2); }
1507
1556
  .asst-login-code { color: var(--asst-text); font-family: var(--asst-mono); font-size: 15px; letter-spacing: .1em; }
1508
1557
 
1509
1558
  .asst-routes-modal { width: min(660px, 100%); }
1510
- .asst-budget-row { align-items: flex-start; }
1559
+ .asst-budget-modal { width: min(560px, calc(100vw - 24px)); }
1560
+ .asst-budget-controls { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: center; min-width: 0; }
1561
+ .asst-budget-controls > :is(.asst-modal-note, .asst-budget-field-error, .asst-budget-switch-label) { grid-column: 1 / -1; }
1562
+ .asst-budget-controls > .asst-btn { justify-self: start; }
1563
+ .asst-budget-controls .asst-budget-input { width: 96px; min-width: 0; box-sizing: border-box; }
1564
+ .asst-budget-switch-label { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--asst-text); cursor: pointer; }
1565
+ .asst-budget-switch { appearance: none; position: relative; width: 30px; height: 18px; margin: 0; border: 0; border-radius: var(--asst-pill-r-xs); background: var(--asst-fill-3); box-shadow: inset 0 0 0 1px var(--asst-line-2); cursor: pointer; }
1566
+ .asst-budget-switch::before { content: ''; position: absolute; width: 12px; height: 12px; top: 3px; left: 3px; border-radius: var(--asst-pill-r-xs); background: var(--asst-text-3); transition: transform var(--asst-dur-fast) var(--asst-ease-standard); }
1567
+ .asst-budget-switch:checked { background: var(--asst-brand); }
1568
+ .asst-budget-switch:checked::before { background: var(--asst-ink); transform: translateX(12px); }
1569
+ .asst-budget-controls :is(input, button):focus-visible { outline: 2px solid var(--asst-focus); outline-offset: 2px; }
1570
+ .asst-budget-controls input:disabled { opacity: .5; }
1571
+ .asst-budget-popover .asst-dd-list { padding: 8px; }
1572
+ .asst-budget-session { display: flex; flex-direction: column; gap: 12px; margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--asst-line); }
1573
+ .asst-budget-total { font-family: var(--asst-mono); }
1574
+ .asst-budget-session[data-tone="warn"] .asst-budget-total, .asst-budget-dry { color: var(--asst-warn); }
1575
+ .asst-budget-session[data-tone="over"] .asst-budget-total { color: var(--asst-live); }
1576
+ .asst-budget-providers { display: flex; flex-direction: column; gap: 8px; }
1577
+ .asst-budget-provider { padding: 8px; border-radius: var(--asst-r-field); background: var(--asst-fill-1); overflow-wrap: anywhere; }
1578
+ .asst-budget-provider[data-eligible="false"] { color: var(--asst-text-3); }
1579
+ .asst-budget-provider-name { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
1580
+ .asst-budget-provider-money { margin-top: 4px; font: 12px/1.5 var(--asst-mono); }
1581
+ .asst-budget-brain { padding: 0 6px; border-radius: var(--asst-pill-r-xs); background: var(--asst-fill-2); font-size: 11.5px; }
1582
+ .asst-budget-dry { font-size: 12px; }
1583
+ .asst-budget-meter { width: 100%; accent-color: var(--asst-brand); }
1511
1584
  .asst-budget-desc, .asst-budget-meta { color: var(--asst-text-3); font-size: 11.5px; line-height: 1.4; }
1512
1585
  .asst-budget-meta { font-variant-numeric: tabular-nums; }
1513
1586
  .asst-budget-field-error { color: var(--asst-live); font-size: 11.5px; line-height: 1.4; }
@@ -1592,7 +1665,7 @@ export function injectAssistantStyles() {
1592
1665
  /* Model routes editor: Settings density on a desktop pointer. Scoped here
1593
1666
  so route cards, the model manager and other Assistant menus keep their UI. */
1594
1667
  .asst-routes-overlay { font-family: var(--stg-font, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Inter, sans-serif); }
1595
- .asst-routes-overlay .asst-routes-modal { width: min(800px, 100%); max-height: calc(100dvh - 32px); border-radius: var(--asst-r-card); line-height: 1.4; }
1668
+ .asst-routes-overlay .asst-routes-modal { width: min(1040px, 100%); max-height: calc(100dvh - 32px); border-radius: var(--asst-r-card); line-height: 1.4; }
1596
1669
  .asst-routes-modal :is(.asst-btn, .asst-dd, .asst-iconbtn) { --cap-h: var(--asst-pill-h-sm); --cap-r: var(--asst-pill-r-xs); --cap-px: var(--asst-pill-px-sm); --cap-icon: 12px; --cap-gap: 4px; }
1597
1670
  .asst-routes-modal .asst-modal-head { min-height: 40px; padding: 8px 14px 8px 22px; font-size: 13px; }
1598
1671
  .asst-routes-modal .asst-modal-head .asst-icon { color: var(--asst-text-2); }
@@ -1616,7 +1689,7 @@ export function injectAssistantStyles() {
1616
1689
  .asst-routes-modal .asst-routes-list-actions .asst-btn:hover:not(:disabled) { color: var(--asst-text); background: var(--asst-fill-1); }
1617
1690
  /* Reserve the widths the current model names, effort words and destinations
1618
1691
  need; give all remaining space to task descriptions instead of a share. */
1619
- .asst-routes-modal .asst-routes-columns, .asst-routes-modal .asst-routes-row { display: grid; grid-template-columns: minmax(0, 1fr) 208px 84px 128px var(--asst-pill-h-sm); column-gap: 6px; }
1692
+ .asst-routes-modal .asst-routes-columns, .asst-routes-modal .asst-routes-row { display: grid; grid-template-columns: minmax(100px, 1fr) minmax(120px, 200px) 70px 116px minmax(120px, 190px) var(--asst-pill-h-sm); column-gap: 6px; }
1620
1693
  .asst-routes-columns { position: sticky; top: -14px; z-index: 1; padding: 4px 8px; color: var(--asst-text-3); font-size: 11.5px; background: var(--asst-popover); }
1621
1694
  .asst-routes-columns span:last-child { visibility: hidden; }
1622
1695
  .asst-routes-modal .asst-routes-list { padding: 0 8px; border: 1px solid var(--asst-line); border-radius: var(--radius-group, 9px); background: var(--asst-field); }
@@ -1655,6 +1728,11 @@ export function injectAssistantStyles() {
1655
1728
  .asst-routes-overlay :is(.asst-route-model-menu, .asst-route-field-menu) .asst-dd-item-main { flex-direction: row; align-items: center; gap: 8px; }
1656
1729
  .asst-routes-overlay :is(.asst-route-model-menu, .asst-route-field-menu) .asst-dd-item-label { flex-shrink: 0; max-width: 60%; }
1657
1730
  .asst-routes-overlay :is(.asst-route-model-menu, .asst-route-field-menu) .asst-dd-item-desc { flex: 1; text-align: end; }
1731
+ /* Account identities and quota windows need a second line in their menu. */
1732
+ .asst-routes-overlay .asst-route-account-menu .asst-dd-item { height: auto; min-height: var(--asst-pill-h-sm); padding: 4px 8px; }
1733
+ .asst-routes-overlay .asst-route-account-menu .asst-dd-item-main { flex-direction: column; align-items: stretch; gap: 2px; }
1734
+ .asst-routes-overlay .asst-route-account-menu :is(.asst-dd-item-label, .asst-dd-item-desc) { white-space: normal; max-width: none; text-align: start; overflow-wrap: anywhere; }
1735
+ .asst-model-menu .asst-dd-item[data-kind="radio"]:not([data-id*=":"]) .asst-dd-item-desc { white-space: normal; overflow-wrap: anywhere; }
1658
1736
  .asst-routes-overlay .asst-route-model-menu .asst-dd-input { height: var(--asst-pill-h-sm); min-height: var(--asst-pill-h-sm); font-size: 12px; border-radius: var(--asst-pill-r-xs); }
1659
1737
  .asst-routes-overlay .asst-route-model-menu .asst-dd-input:focus-visible { outline: 2px solid var(--asst-focus); outline-offset: -2px; }
1660
1738
  .asst-route-menu-foot { flex-shrink: 0; display: flex; flex-wrap: wrap; align-items: center; border-top: 1px solid var(--asst-line-2); padding-top: 4px; }
@@ -1665,7 +1743,7 @@ export function injectAssistantStyles() {
1665
1743
  .asst-routes-modal :is(.asst-btn, .asst-dd, .asst-iconbtn) { --cap-h: 44px; }
1666
1744
  .asst-routes-modal .asst-routes-mode { height: auto; min-height: 44px; }
1667
1745
  .asst-routes-modal .asst-routes-mode-label { white-space: normal; }
1668
- .asst-routes-modal .asst-routes-columns, .asst-routes-modal .asst-routes-row { grid-template-columns: minmax(0, 1.15fr) minmax(0, 1.55fr) minmax(0, .7fr) minmax(0, .9fr) 44px; }
1746
+ .asst-routes-modal .asst-routes-columns, .asst-routes-modal .asst-routes-row { grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr) minmax(0, .7fr) minmax(0, .9fr) minmax(0, 1.4fr) 44px; }
1669
1747
  .asst-routes-overlay :is(.asst-route-model-menu, .asst-route-field-menu) :is(.asst-dd-item, .asst-dd-input) { min-height: 44px; height: auto; padding: 8px; }
1670
1748
  .asst-routes-overlay :is(.asst-route-model-menu, .asst-route-field-menu) .asst-dd-item-main { flex-direction: column; align-items: stretch; gap: 2px; }
1671
1749
  .asst-routes-overlay :is(.asst-route-model-menu, .asst-route-field-menu) :is(.asst-dd-item-label, .asst-dd-item-desc) { white-space: normal; max-width: none; text-align: start; overflow-wrap: anywhere; }
@@ -1678,7 +1756,7 @@ export function injectAssistantStyles() {
1678
1756
  .asst-routes-modal .asst-routes-modes { grid-template-columns: 1fr; }
1679
1757
  .asst-routes-modal .asst-routes-columns { display: none; }
1680
1758
  .asst-routes-modal .asst-routes-row { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 44px; gap: 8px; padding: 12px 0; }
1681
- .asst-routes-modal .asst-routes-row-main, .asst-routes-modal .asst-routes-model-cell { grid-column: 1 / -1; }
1759
+ .asst-routes-modal .asst-routes-row-main, .asst-routes-modal .asst-routes-model-cell, .asst-routes-modal .asst-routes-account-cell { grid-column: 1 / -1; }
1682
1760
  .asst-routes-cell { flex-direction: column; gap: 4px; }
1683
1761
  .asst-routes-cell-label { display: block; color: var(--asst-text-3); font-size: 11.5px; }
1684
1762
  .asst-routes-reset-cell { align-self: end; }
@@ -1790,8 +1868,8 @@ export function injectAssistantStyles() {
1790
1868
  .asst-suggestions { flex-direction: column; align-items: stretch; width: 100%; }
1791
1869
  .asst-chip.asst-suggestion { justify-content: flex-start; max-width: none; }
1792
1870
  .asst-empty-project { width: 100%; }
1793
- .asst-dd.asst-empty-project-btn { flex: 1 1 auto; justify-content: flex-start; max-width: none; }
1794
- .asst-empty-project-btn .asst-dd-label { flex: 1 1 auto; max-width: none; text-align: start; }
1871
+ .asst-dd.asst-empty-control-btn { flex: 1 1 auto; justify-content: flex-start; max-width: none; }
1872
+ .asst-empty-control-btn .asst-dd-label { flex: 1 1 auto; max-width: none; text-align: start; }
1795
1873
  .asst-msg.msg-user .asst-msg-content { max-width: 92%; }
1796
1874
  .asst-dd-model .asst-dd-label { max-width: 9ch; }
1797
1875
  .asst-route-opt-sub { display: none; }
@@ -1825,6 +1903,8 @@ export function injectAssistantStyles() {
1825
1903
  @keyframes asst-spin { to { transform: rotate(360deg); } }
1826
1904
 
1827
1905
  body.ui-interacting :is(.asst-live-dot, .asst-spinner, .asst-computer-dot, .syna-cameo-eye) { animation-play-state: paused !important; }
1906
+ body.ui-interacting .asst-route-line[data-live] .asst-route-icon,
1907
+ body.ui-interacting .asst-route-line[data-live] .asst-route-sweep::after { animation-play-state: paused !important; }
1828
1908
 
1829
1909
  @media (prefers-reduced-motion: reduce) {
1830
1910
  .asst-transcript { scroll-behavior: auto; }
@@ -1834,6 +1914,8 @@ export function injectAssistantStyles() {
1834
1914
  .term-minimized-pill,
1835
1915
  .term-minimized-pill::before,
1836
1916
  .term-minimized-pill::after { animation: none !important; transition: none !important; }
1917
+ /* A frozen sweep would be a stray stub: the route row draws none (its run card and state words remain). */
1918
+ .asst-route-sweep { display: none; }
1837
1919
  }
1838
1920
  `;
1839
1921
  document.head.appendChild(style);
@@ -20,6 +20,8 @@ const definitions = [
20
20
  ['hooks', 'Inspect configured hooks', 'hooks_list'],
21
21
  ['init', 'Create or update AGENTS.md guidance', null, true],
22
22
  ['logout', 'Sign out of Codex', 'account_logout'],
23
+ ['login', 'Sign in with ChatGPT'],
24
+ ['account', 'Choose an account or sign in'],
23
25
  ['mcp', 'Search MCP servers and tools', 'mcp_status'],
24
26
  ['mention', 'Add a structured file mention', null, true],
25
27
  ['model', 'Choose model'],
@@ -146,10 +148,11 @@ export function configChanges(definitions, inputs, original) {
146
148
  export const SESSION_CONFIG_FIELDS = Object.freeze({
147
149
  model: 'model', model_reasoning_effort: 'effort', model_reasoning_summary: 'summary',
148
150
  personality: 'personality', service_tier: 'serviceTier', approval_policy: 'approvalPolicy',
151
+ sandbox_mode: 'sandboxPolicy',
149
152
  });
150
153
 
151
154
  export function sessionSettings(config) {
152
- return Object.fromEntries(Object.entries(config).filter(([key]) => SESSION_CONFIG_FIELDS[key]).map(([key, value]) => [SESSION_CONFIG_FIELDS[key], value]));
155
+ return Object.fromEntries(Object.entries(config).filter(([key]) => SESSION_CONFIG_FIELDS[key]).map(([key, value]) => [key === 'sandbox_mode' ? 'sandboxMode' : SESSION_CONFIG_FIELDS[key], value]));
153
156
  }
154
157
 
155
158
  export function modelServiceTiers(model) {