acdev 1.0.7 → 1.0.9

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.
package/public/app.js CHANGED
@@ -130,11 +130,34 @@ let settingsTab = 'ticket';
130
130
  /** Sentinel: no model selected (jobs blocked until user picks one). */
131
131
  const NO_MODEL = '-';
132
132
  /** @type {string} */
133
+ let modelSourceHintBase = '';
134
+
135
+ /**
136
+ * @param {HTMLElement} btn
137
+ * @param {{ id: string, label?: string, name?: string }} model
138
+ */
139
+ function populateModelPickerOption(btn, model) {
140
+ const title = document.createElement('span');
141
+ title.className = 'model-picker-option-title';
142
+ title.textContent = modelPickerLabel(model);
143
+ btn.appendChild(title);
144
+
145
+ if (model.id !== modelPickerLabel(model)) {
146
+ const sub = document.createElement('span');
147
+ sub.className = 'model-picker-option-id';
148
+ sub.textContent = model.id;
149
+ btn.appendChild(sub);
150
+ }
151
+ }
152
+
153
+ function refreshModelSourceHint() {
154
+ if (!els.settingsModelHint) return;
155
+ els.settingsModelHint.textContent = modelSourceHintBase;
156
+ }
157
+ /** @type {string} */
133
158
  let currentModel = 'claude-sonnet-5';
134
159
  /** @type {Array<{id:string,label?:string,name?:string}>} */
135
160
  let availableModels = [];
136
- /** @type {'claude' | 'openrouter' | null} */
137
- let availableModelsProvider = null;
138
161
  /** @type {'github' | 'jira'} */
139
162
  let ticketSource = 'github';
140
163
  /** @type {{
@@ -167,11 +190,6 @@ let ticketSource = 'github';
167
190
  * anthropicApiKeyMasked?: string | null,
168
191
  * claudeOauthTokenSet?: boolean,
169
192
  * claudeOauthTokenMasked?: string | null,
170
- * llmProvider?: 'claude' | 'openrouter',
171
- * lastModelsByProvider?: Record<string, string>,
172
- * openrouterAuthOk?: boolean,
173
- * openrouterApiKeySet?: boolean,
174
- * openrouterApiKeyMasked?: string | null,
175
193
  * llmAuthOk?: boolean,
176
194
  * stubAgent?: boolean,
177
195
  * }} */
@@ -187,7 +205,6 @@ const els = {
187
205
  modelProvider: document.getElementById('model-provider'),
188
206
  modelCurrent: document.getElementById('model-current'),
189
207
  modelCombined: document.getElementById('model-combined'),
190
- overviewLlmProvider: document.getElementById('overview-llm-provider'),
191
208
  overviewModel: document.getElementById('overview-model'),
192
209
  overviewAgentPicker: document.getElementById('overview-agent-picker'),
193
210
  overviewAgentTrigger: document.getElementById('overview-agent-trigger'),
@@ -273,8 +290,6 @@ const els = {
273
290
  settingsModelList: document.getElementById('settings-model-list'),
274
291
  settingsModelSearch: document.getElementById('settings-model-search'),
275
292
  settingsModelHint: document.getElementById('settings-model-hint'),
276
- settingsLlmProvider: document.getElementById('settings-llm-provider'),
277
- settingsLlmProviderHint: document.getElementById('settings-llm-provider-hint'),
278
293
  settingsMaxTurns: document.getElementById('settings-max-turns'),
279
294
  settingsTimeout: document.getElementById('settings-timeout'),
280
295
  settingsTimeoutMs: document.getElementById('settings-timeout-ms'),
@@ -315,10 +330,6 @@ const els = {
315
330
  settingsClaudeOauth: document.getElementById('settings-claude-oauth'),
316
331
  settingsClaudeOauthClear: document.getElementById('settings-claude-oauth-clear'),
317
332
  settingsClaudeOauthHint: document.getElementById('settings-claude-oauth-hint'),
318
- settingsOpenrouterStatus: document.getElementById('settings-openrouter-status'),
319
- settingsOpenrouterKey: document.getElementById('settings-openrouter-key'),
320
- settingsOpenrouterKeyClear: document.getElementById('settings-openrouter-key-clear'),
321
- settingsOpenrouterKeyHint: document.getElementById('settings-openrouter-key-hint'),
322
333
  settingsTabs: document.getElementById('settings-tabs'),
323
334
  };
324
335
 
@@ -2975,34 +2986,21 @@ async function readJson(res) {
2975
2986
  }
2976
2987
  }
2977
2988
 
2978
- function llmProviderLabel(provider) {
2979
- return provider === 'openrouter' ? 'OpenRouter' : 'Claude';
2980
- }
2981
-
2982
2989
  /**
2983
- * @param {'claude' | 'openrouter'} [provider]
2984
2990
  * @param {string} [modelId]
2985
2991
  * @returns {string}
2986
2992
  */
2987
- function agentDisplayText(provider = currentLlmProviderSelection(), modelId = currentModel) {
2988
- const providerName = llmProviderLabel(provider);
2989
- const modelText = isNoModelSelection(modelId) ? '—' : modelPickerDisplayText(modelId);
2990
- return `${providerName} • ${modelText}`;
2993
+ function agentDisplayText(modelId = currentModel) {
2994
+ return isNoModelSelection(modelId) ? '—' : modelPickerDisplayText(modelId);
2991
2995
  }
2992
2996
 
2993
2997
  function updateModelLabel() {
2994
- const provider = appConfig.llmProvider === 'openrouter' ? 'openrouter' : 'claude';
2995
- const providerName = llmProviderLabel(provider);
2996
2998
  const modelText = isNoModelSelection(currentModel) ? '—' : currentModel;
2997
2999
  const modelTitle = isNoModelSelection(currentModel)
2998
3000
  ? 'none selected'
2999
3001
  : currentModel;
3000
- const combined = agentDisplayText(provider, currentModel);
3002
+ const combined = agentDisplayText(currentModel);
3001
3003
 
3002
- if (els.modelProvider) {
3003
- els.modelProvider.textContent = providerName;
3004
- els.modelProvider.title = `${providerName} — switch in Overview or Settings`;
3005
- }
3006
3004
  if (els.modelCurrent) {
3007
3005
  els.modelCurrent.textContent = modelText;
3008
3006
  els.modelCurrent.title = `Model: ${modelTitle} — switch in Overview or Settings`;
@@ -3012,29 +3010,19 @@ function updateModelLabel() {
3012
3010
  els.modelCombined.title = combined;
3013
3011
  }
3014
3012
  syncAgentPickerDisplay();
3013
+ refreshModelSourceHint();
3015
3014
  }
3016
3015
 
3017
- /**
3018
- * @param {'claude' | 'openrouter'} [provider]
3019
- * @returns {Array<{id:string,label?:string,name?:string}>}
3020
- */
3021
- function getAvailableModels(provider = currentLlmProviderSelection()) {
3022
- if (availableModelsProvider === provider) return availableModels;
3023
- return [];
3016
+ function getAvailableModels() {
3017
+ return availableModels.length ? availableModels : defaultModels();
3024
3018
  }
3025
3019
 
3026
- /**
3027
- * @param {'claude' | 'openrouter'} provider
3028
- * @param {Array<{id:string,label?:string,name?:string}>} models
3029
- */
3030
- function setAvailableModels(provider, models) {
3031
- availableModelsProvider = provider;
3020
+ function setAvailableModels(models) {
3032
3021
  availableModels = models;
3033
3022
  }
3034
3023
 
3035
3024
  function clearAvailableModels() {
3036
3025
  availableModels = [];
3037
- availableModelsProvider = null;
3038
3026
  }
3039
3027
 
3040
3028
  /**
@@ -3281,16 +3269,7 @@ function renderModelComboboxList(instance, selectedId = currentModel) {
3281
3269
  btn.classList.add('is-active');
3282
3270
  }
3283
3271
 
3284
- const title = document.createElement('span');
3285
- title.textContent = modelPickerLabel(m);
3286
- btn.appendChild(title);
3287
-
3288
- if (m.id !== modelPickerLabel(m)) {
3289
- const sub = document.createElement('span');
3290
- sub.className = 'model-picker-option-id';
3291
- sub.textContent = m.id;
3292
- btn.appendChild(sub);
3293
- }
3272
+ populateModelPickerOption(btn, m);
3294
3273
 
3295
3274
  btn.addEventListener('click', () => {
3296
3275
  instance.onSelect(m.id);
@@ -3330,76 +3309,8 @@ function renderAllModelComboboxLists(selectedId = currentModel) {
3330
3309
  /** @type {AgentPickerInstance[]} */
3331
3310
  const agentPickers = [];
3332
3311
 
3333
- const PROVIDER_UNAVAILABLE_TOOLTIP =
3334
- 'Not configured — add credentials in Settings → Authentication';
3335
-
3336
- /**
3337
- * @param {'claude' | 'openrouter'} provider
3338
- * @param {typeof appConfig} [cfg]
3339
- */
3340
- function isLlmProviderAvailable(provider, cfg = appConfig) {
3341
- if (cfg.stubAgent) return true;
3342
- if (provider === 'openrouter') return cfg.openrouterAuthOk === true;
3343
- return cfg.claudeAuthOk === true;
3344
- }
3345
-
3346
- /**
3347
- * Disable provider tabs and select options when auth is missing.
3348
- * @param {typeof appConfig} [cfg]
3349
- */
3350
- function syncLlmProviderAvailability(cfg = appConfig) {
3351
- for (const ap of agentPickers) {
3352
- for (const tab of ap.tabs) {
3353
- const provider =
3354
- tab.dataset.provider === 'openrouter' ? 'openrouter' : 'claude';
3355
- const available = isLlmProviderAvailable(provider, cfg);
3356
- tab.disabled = !available;
3357
- tab.classList.toggle('is-disabled', !available);
3358
- tab.setAttribute('aria-disabled', available ? 'false' : 'true');
3359
- if (available) {
3360
- tab.removeAttribute('title');
3361
- } else {
3362
- tab.title = PROVIDER_UNAVAILABLE_TOOLTIP;
3363
- }
3364
- }
3365
- }
3366
-
3367
- for (const select of [els.settingsLlmProvider, els.overviewLlmProvider]) {
3368
- if (!select) continue;
3369
- for (const opt of select.options) {
3370
- const provider = opt.value === 'openrouter' ? 'openrouter' : 'claude';
3371
- const available = isLlmProviderAvailable(provider, cfg);
3372
- opt.disabled = !available;
3373
- if (available) {
3374
- opt.removeAttribute('title');
3375
- } else {
3376
- opt.title = PROVIDER_UNAVAILABLE_TOOLTIP;
3377
- }
3378
- }
3379
- }
3380
- }
3381
-
3382
- /**
3383
- * @param {'claude' | 'openrouter'} provider
3384
- */
3385
- function syncAgentPickerTabs(provider) {
3386
- const value = provider === 'openrouter' ? 'openrouter' : 'claude';
3387
- for (const ap of agentPickers) {
3388
- for (const tab of ap.tabs) {
3389
- const active = tab.dataset.provider === value;
3390
- tab.classList.toggle('is-active', active);
3391
- tab.setAttribute('aria-selected', active ? 'true' : 'false');
3392
- }
3393
- if (ap.iconEl) {
3394
- ap.iconEl.classList.toggle('agent-pill-icon--claude', value === 'claude');
3395
- ap.iconEl.classList.toggle('agent-pill-icon--openrouter', value === 'openrouter');
3396
- }
3397
- }
3398
- }
3399
-
3400
3312
  function syncAgentPickerDisplay(selectedId = currentModel) {
3401
- const provider = currentLlmProviderSelection();
3402
- const text = agentDisplayText(provider, selectedId);
3313
+ const text = agentDisplayText(selectedId);
3403
3314
  for (const ap of agentPickers) {
3404
3315
  ap.valueEl.textContent = text;
3405
3316
  ap.valueEl.classList.toggle('is-empty', isNoModelSelection(selectedId));
@@ -3407,7 +3318,6 @@ function syncAgentPickerDisplay(selectedId = currentModel) {
3407
3318
  ap.hiddenInput.value = isNoModelSelection(selectedId) ? NO_MODEL : selectedId;
3408
3319
  }
3409
3320
  }
3410
- syncAgentPickerTabs(provider);
3411
3321
  }
3412
3322
 
3413
3323
  /**
@@ -3431,7 +3341,6 @@ function setAgentPickerOpen(instance, open) {
3431
3341
  if (open) {
3432
3342
  instance.activeIndex = -1;
3433
3343
  instance.search.value = '';
3434
- syncAgentPickerTabs(currentLlmProviderSelection());
3435
3344
  renderAgentPickerModelList(instance, currentModel);
3436
3345
  focusWithoutScroll(instance.search);
3437
3346
  } else {
@@ -3477,16 +3386,7 @@ function renderAgentPickerModelList(instance, selectedId = currentModel) {
3477
3386
  btn.classList.add('is-active');
3478
3387
  }
3479
3388
 
3480
- const title = document.createElement('span');
3481
- title.textContent = modelPickerLabel(m);
3482
- btn.appendChild(title);
3483
-
3484
- if (m.id !== modelPickerLabel(m)) {
3485
- const sub = document.createElement('span');
3486
- sub.className = 'model-picker-option-id';
3487
- sub.textContent = m.id;
3488
- btn.appendChild(sub);
3489
- }
3389
+ populateModelPickerOption(btn, m);
3490
3390
 
3491
3391
  btn.addEventListener('click', () => {
3492
3392
  instance.onSelect(m.id);
@@ -3577,19 +3477,6 @@ function initAgentPicker(opts) {
3577
3477
  }
3578
3478
  });
3579
3479
 
3580
- for (const tab of instance.tabs) {
3581
- tab.addEventListener('click', () => {
3582
- if (tab.disabled) return;
3583
- const provider =
3584
- tab.dataset.provider === 'openrouter' ? 'openrouter' : 'claude';
3585
- if (!isLlmProviderAvailable(provider)) return;
3586
- if (provider === currentLlmProviderSelection()) return;
3587
- syncLlmProviderSelects(provider);
3588
- syncAgentPickerTabs(provider);
3589
- void onLlmProviderChange(provider);
3590
- });
3591
- }
3592
-
3593
3480
  return instance;
3594
3481
  }
3595
3482
 
@@ -3622,26 +3509,10 @@ function reconcileModelForProvider(models, candidate = currentModel, storedCandi
3622
3509
  return NO_MODEL;
3623
3510
  }
3624
3511
 
3625
- /**
3626
- * @param {'claude' | 'openrouter'} provider
3627
- * @returns {string | undefined}
3628
- */
3629
- function lastModelForProvider(provider) {
3630
- const stored = appConfig.lastModelsByProvider?.[provider];
3631
- return typeof stored === 'string' && !isNoModelSelection(stored) ? stored : undefined;
3632
- }
3633
-
3634
3512
  /**
3635
3513
  * @returns {Array<{id:string,label?:string,name?:string}>}
3636
3514
  */
3637
- function defaultModelsForProvider(provider = 'claude') {
3638
- if (provider === 'openrouter') {
3639
- return [
3640
- { id: 'anthropic/claude-sonnet-4', label: 'Claude Sonnet 4 (Anthropic)' },
3641
- { id: 'anthropic/claude-opus-4', label: 'Claude Opus 4 (Anthropic)' },
3642
- { id: 'anthropic/claude-3.5-sonnet', label: 'Claude 3.5 Sonnet (Anthropic)' },
3643
- ];
3644
- }
3515
+ function defaultModels() {
3645
3516
  return [
3646
3517
  { id: 'claude-sonnet-5', label: 'Sonnet 5' },
3647
3518
  { id: 'claude-opus-5', label: 'Opus 5' },
@@ -3653,29 +3524,20 @@ function defaultModelsForProvider(provider = 'claude') {
3653
3524
  }
3654
3525
 
3655
3526
  /**
3656
- * @param {'anthropic' | 'openrouter' | 'fallback' | string | undefined} source
3657
- * @param {'claude' | 'openrouter'} [provider]
3527
+ * @param {'anthropic' | 'fallback' | string | undefined} source
3658
3528
  */
3659
- function updateModelSourceHint(source, provider = 'claude') {
3529
+ function updateModelSourceHint(source) {
3660
3530
  if (!els.settingsModelHint) return;
3661
- const name = provider === 'openrouter' ? 'OpenRouter' : 'Claude';
3662
3531
  if (source === 'anthropic') {
3663
- els.settingsModelHint.textContent =
3664
- `${name} model for agent runs. List loaded from Anthropic Models API.`;
3665
- } else if (source === 'openrouter') {
3666
- els.settingsModelHint.textContent =
3667
- 'OpenRouter model for agent runs (anthropic/* only — Claude Agent SDK uses Anthropic-compatible API). List loaded from OpenRouter Models API.';
3532
+ modelSourceHintBase =
3533
+ 'Claude model for agent runs. List loaded from Anthropic Models API.';
3668
3534
  } else if (source === 'fallback') {
3669
- if (provider === 'openrouter') {
3670
- els.settingsModelHint.textContent =
3671
- 'OpenRouter model for agent runs (anthropic/* only). Showing curated Anthropic models (live list unavailable — set OPENROUTER_API_KEY in Authentication).';
3672
- } else {
3673
- els.settingsModelHint.textContent =
3674
- 'Claude model for agent runs. Showing curated Claude Code models (live list unavailable — set an API key or use claude auth login).';
3675
- }
3535
+ modelSourceHintBase =
3536
+ 'Claude model for agent runs. Showing curated Claude Code models (live list unavailable — set an API key or use claude auth login).';
3676
3537
  } else {
3677
- els.settingsModelHint.textContent = `${name} model for agent runs.`;
3538
+ modelSourceHintBase = 'Claude model for agent runs.';
3678
3539
  }
3540
+ refreshModelSourceHint();
3679
3541
  }
3680
3542
 
3681
3543
  /**
@@ -3699,90 +3561,55 @@ function modelMatchesSearchQuery(model, query) {
3699
3561
  .some((part) => String(part).toLowerCase().includes(q));
3700
3562
  }
3701
3563
 
3702
- /**
3703
- * @returns {'claude' | 'openrouter'}
3704
- */
3705
- function currentLlmProviderSelection() {
3706
- const fromOverview = els.overviewLlmProvider?.value;
3707
- if (fromOverview === 'openrouter' || fromOverview === 'claude') return fromOverview;
3708
- const fromForm = els.settingsLlmProvider?.value;
3709
- if (fromForm === 'openrouter' || fromForm === 'claude') return fromForm;
3710
- return appConfig.llmProvider === 'openrouter' ? 'openrouter' : 'claude';
3711
- }
3712
-
3713
- /**
3714
- * @param {'claude' | 'openrouter'} provider
3715
- */
3716
- function syncLlmProviderSelects(provider) {
3717
- const value = provider === 'openrouter' ? 'openrouter' : 'claude';
3718
- if (els.settingsLlmProvider) els.settingsLlmProvider.value = value;
3719
- if (els.overviewLlmProvider) els.overviewLlmProvider.value = value;
3720
- syncAgentPickerTabs(value);
3721
- if (els.settingsLlmProviderHint) {
3722
- els.settingsLlmProviderHint.textContent =
3723
- value === 'openrouter'
3724
- ? 'Agent runs route through OpenRouter (requires OPENROUTER_API_KEY). Use anthropic/* models only.'
3725
- : 'Agent runs use Claude via Anthropic API, OAuth token, or claude auth login.';
3726
- }
3727
- }
3728
-
3729
- /**
3730
- * @param {{ models?: Array<{id:string,label?:string,name?:string}>, selected?: string, source?: string, provider?: string }} data
3731
- */
3732
3564
  function populateSettingsModels(data) {
3733
- const provider = data?.provider || currentLlmProviderSelection();
3734
- const cached = getAvailableModels(provider);
3735
- const models = Array.isArray(data?.models)
3565
+ let models = Array.isArray(data?.models)
3736
3566
  ? data.models
3737
- : data?.reconcileProvider
3567
+ : data?.reconcile
3738
3568
  ? []
3739
- : cached.length
3740
- ? cached
3741
- : defaultModelsForProvider(provider);
3742
- setAvailableModels(provider, models);
3569
+ : getAvailableModels().length
3570
+ ? getAvailableModels()
3571
+ : defaultModels();
3572
+ if (!models.length && data?.source) {
3573
+ models = defaultModels();
3574
+ }
3575
+ setAvailableModels(models);
3743
3576
 
3744
- if (data?.reconcileProvider) {
3577
+ if (data?.reconcile) {
3745
3578
  currentModel = reconcileModelForProvider(
3746
3579
  models,
3747
- data.reconcileCandidate,
3748
- lastModelForProvider(provider)
3580
+ data.reconcileCandidate
3749
3581
  );
3750
3582
  } else if (data?.selected != null) {
3751
3583
  const next = isNoModelSelection(data.selected) ? NO_MODEL : data.selected;
3752
- currentModel = reconcileModelForProvider(models, next, lastModelForProvider(provider));
3584
+ currentModel = reconcileModelForProvider(models, next);
3753
3585
  }
3754
- if (data?.source) updateModelSourceHint(data.source, provider);
3586
+ if (data?.source) updateModelSourceHint(data.source);
3755
3587
 
3756
3588
  syncAllModelComboboxValues(currentModel);
3757
3589
  renderAllModelComboboxLists(currentModel);
3758
3590
  updateModelLabel();
3759
3591
  }
3760
3592
 
3761
- function modelsForFetchFallback(provider, reconcileProvider) {
3762
- if (reconcileProvider) return [];
3763
- const cached = getAvailableModels(provider);
3764
- if (cached.length) return cached;
3765
- return defaultModelsForProvider(provider);
3593
+ function modelsForFetchFallback() {
3594
+ return getAvailableModels().length ? getAvailableModels() : defaultModels();
3766
3595
  }
3767
3596
 
3768
3597
  async function fetchModels(opts = {}) {
3769
3598
  const refresh = opts.refresh === true;
3770
- const provider = opts.provider || currentLlmProviderSelection();
3771
- const reconcileProvider = opts.reconcileProvider === true;
3599
+ const reconcile = opts.reconcile === true;
3772
3600
  const reconcileCandidate = opts.reconcileCandidate;
3773
- if (reconcileProvider) {
3601
+ if (reconcile) {
3774
3602
  clearAvailableModels();
3775
3603
  }
3776
- const query = new URLSearchParams({ provider });
3604
+ const query = new URLSearchParams();
3777
3605
  if (refresh) query.set('refresh', '1');
3778
3606
  try {
3779
3607
  const res = await fetch(`/api/models?${query.toString()}`);
3780
3608
  if (!res.ok) {
3781
3609
  populateSettingsModels({
3782
- models: modelsForFetchFallback(provider, reconcileProvider),
3610
+ models: modelsForFetchFallback(),
3783
3611
  selected: currentModel,
3784
- provider,
3785
- reconcileProvider,
3612
+ reconcile,
3786
3613
  reconcileCandidate,
3787
3614
  });
3788
3615
  return;
@@ -3790,17 +3617,15 @@ async function fetchModels(opts = {}) {
3790
3617
  const data = await readJson(res);
3791
3618
  populateSettingsModels({
3792
3619
  ...data,
3793
- provider: data.provider || provider,
3794
- reconcileProvider,
3620
+ reconcile,
3795
3621
  reconcileCandidate,
3796
3622
  });
3797
3623
  } catch (err) {
3798
3624
  console.error('Failed to fetch models:', err);
3799
3625
  populateSettingsModels({
3800
- models: modelsForFetchFallback(provider, reconcileProvider),
3626
+ models: modelsForFetchFallback(),
3801
3627
  selected: currentModel,
3802
- provider,
3803
- reconcileProvider,
3628
+ reconcile,
3804
3629
  reconcileCandidate,
3805
3630
  });
3806
3631
  }
@@ -3920,13 +3745,6 @@ function claudeStatusText(cfg) {
3920
3745
  }
3921
3746
  }
3922
3747
 
3923
- function openrouterStatusText(cfg) {
3924
- if (cfg.openrouterAuthOk) {
3925
- return 'Authenticated via OPENROUTER_API_KEY';
3926
- }
3927
- return 'Not authenticated — add an OpenRouter API key below';
3928
- }
3929
-
3930
3748
  function fillAuthSettings(cfg) {
3931
3749
  if (els.settingsGhStatus) {
3932
3750
  els.settingsGhStatus.textContent = ghStatusText(cfg);
@@ -3985,25 +3803,6 @@ function fillAuthSettings(cfg) {
3985
3803
  ? 'OAuth token stored in <code>.acdev/.env</code> as <code>CLAUDE_CODE_OAUTH_TOKEN</code>. Settings cannot complete browser OAuth — that still needs <code>claude auth login</code> on this host.'
3986
3804
  : 'From <code>claude setup-token</code> for non-interactive subscription auth. Stored as <code>CLAUDE_CODE_OAUTH_TOKEN</code>. Settings cannot complete browser OAuth — that still needs <code>claude auth login</code> on this host.';
3987
3805
  }
3988
-
3989
- if (els.settingsOpenrouterStatus) {
3990
- els.settingsOpenrouterStatus.textContent = openrouterStatusText(cfg);
3991
- els.settingsOpenrouterStatus.className = cfg.openrouterAuthOk
3992
- ? 'auth-status ok'
3993
- : 'auth-status err';
3994
- }
3995
- fillSecretInput(
3996
- els.settingsOpenrouterKey,
3997
- els.settingsOpenrouterKeyClear,
3998
- Boolean(cfg.openrouterApiKeySet),
3999
- cfg.openrouterApiKeyMasked,
4000
- 'Paste an OpenRouter API key'
4001
- );
4002
- if (els.settingsOpenrouterKeyHint) {
4003
- els.settingsOpenrouterKeyHint.innerHTML = cfg.openrouterApiKeySet
4004
- ? 'API key stored in <code>.acdev/.env</code> as <code>OPENROUTER_API_KEY</code> (not committed).'
4005
- : 'Required when LLM provider is OpenRouter. Get a key at <a href="https://openrouter.ai/keys" target="_blank" rel="noopener noreferrer">openrouter.ai/keys</a>. Stored as <code>OPENROUTER_API_KEY</code> in <code>.acdev/.env</code>.';
4006
- }
4007
3806
  }
4008
3807
 
4009
3808
  function fillSettingsForm(cfg) {
@@ -4083,23 +3882,17 @@ function fillSettingsForm(cfg) {
4083
3882
  els.settingsBaseBranch.value = cfg.baseBranch || '';
4084
3883
  }
4085
3884
 
4086
- if (els.settingsLlmProvider) {
4087
- syncLlmProviderSelects(cfg.llmProvider === 'openrouter' ? 'openrouter' : 'claude');
4088
- }
4089
-
4090
3885
  if (els.settingsModelPicker) {
4091
- const provider = cfg.llmProvider === 'openrouter' ? 'openrouter' : 'claude';
4092
- if (getAvailableModels(provider).length) {
3886
+ if (getAvailableModels().length) {
4093
3887
  populateSettingsModels({
4094
- models: getAvailableModels(provider),
3888
+ models: getAvailableModels(),
4095
3889
  selected: cfg.model ?? currentModel,
4096
- provider,
4097
3890
  });
4098
3891
  } else if (cfg.model != null) {
4099
3892
  currentModel = isNoModelSelection(cfg.model) ? NO_MODEL : cfg.model;
4100
3893
  syncModelPickerValue(currentModel);
4101
3894
  }
4102
- void fetchModels({ provider });
3895
+ void fetchModels();
4103
3896
  }
4104
3897
 
4105
3898
  if (els.settingsMaxTurns) {
@@ -4292,16 +4085,14 @@ function setOverviewLlmFeedback(message, kind = 'ok') {
4292
4085
  */
4293
4086
  function fillOverviewLlmControls(cfg) {
4294
4087
  if (!cfg) return;
4295
- const provider = cfg.llmProvider === 'openrouter' ? 'openrouter' : 'claude';
4296
- syncLlmProviderSelects(provider);
4297
4088
  syncAllModelComboboxValues(currentModel);
4298
4089
  }
4299
4090
 
4300
4091
  let llmConfigSaving = false;
4301
4092
 
4302
4093
  /**
4303
- * PATCH llmProvider and/or model immediately (Overview switcher).
4304
- * @param {{ llmProvider?: 'claude' | 'openrouter', model?: string }} patch
4094
+ * PATCH model immediately (Overview switcher).
4095
+ * @param {{ model?: string }} patch
4305
4096
  */
4306
4097
  async function patchLlmConfig(patch) {
4307
4098
  if (llmConfigSaving) return;
@@ -4323,7 +4114,7 @@ async function patchLlmConfig(patch) {
4323
4114
  if (currentView === 'settings') {
4324
4115
  fillSettingsForm(data);
4325
4116
  }
4326
- setOverviewLlmFeedback('LLM updated. Next jobs will use these values.', 'ok');
4117
+ setOverviewLlmFeedback('Model updated. Next jobs will use this value.', 'ok');
4327
4118
  window.setTimeout(() => setOverviewLlmFeedback(null), 2500);
4328
4119
  } catch (err) {
4329
4120
  setOverviewLlmFeedback(err.message || 'Update failed', 'error');
@@ -4332,29 +4123,6 @@ async function patchLlmConfig(patch) {
4332
4123
  }
4333
4124
  }
4334
4125
 
4335
- /**
4336
- * @param {'claude' | 'openrouter'} provider
4337
- */
4338
- async function onLlmProviderChange(provider) {
4339
- const previousModel = currentModel;
4340
- syncLlmProviderSelects(provider);
4341
- clearAvailableModels();
4342
- currentModel = NO_MODEL;
4343
- syncAllModelComboboxValues(currentModel);
4344
- renderAllModelComboboxLists(currentModel);
4345
- updateModelLabel();
4346
- await fetchModels({
4347
- refresh: true,
4348
- provider,
4349
- reconcileProvider: true,
4350
- reconcileCandidate: previousModel,
4351
- });
4352
- await patchLlmConfig({
4353
- llmProvider: provider,
4354
- model: currentModel,
4355
- });
4356
- }
4357
-
4358
4126
  function applyConfigSnapshot(data) {
4359
4127
  appConfig = data || {};
4360
4128
  if (data?.model != null) {
@@ -4370,7 +4138,6 @@ function applyConfigSnapshot(data) {
4370
4138
  if (els.repoBranch) {
4371
4139
  els.repoBranch.textContent = data?.baseBranch || '—';
4372
4140
  }
4373
- syncLlmProviderAvailability(appConfig);
4374
4141
  if (currentView === 'settings') {
4375
4142
  fillSettingsForm(appConfig);
4376
4143
  }
@@ -4379,11 +4146,13 @@ function applyConfigSnapshot(data) {
4379
4146
  async function fetchConfig() {
4380
4147
  try {
4381
4148
  const res = await fetch('/api/config');
4382
- if (!res.ok) return;
4149
+ if (!res.ok) return null;
4383
4150
  const data = await readJson(res);
4384
4151
  applyConfigSnapshot(data);
4152
+ return data;
4385
4153
  } catch (err) {
4386
4154
  console.error('Failed to fetch config:', err);
4155
+ return null;
4387
4156
  }
4388
4157
  }
4389
4158
 
@@ -4404,7 +4173,6 @@ function readSettingsForm() {
4404
4173
  const patch = {
4405
4174
  baseBranch,
4406
4175
  model,
4407
- llmProvider: els.settingsLlmProvider?.value === 'openrouter' ? 'openrouter' : 'claude',
4408
4176
  maxAgentTurns,
4409
4177
  agentTimeoutMs: Math.round(timeoutMinutes * 60_000),
4410
4178
  testCommand: testTrimmed === '' ? null : testTrimmed,
@@ -4418,8 +4186,6 @@ function readSettingsForm() {
4418
4186
  if (anthropicApiKey !== undefined) patch.anthropicApiKey = anthropicApiKey;
4419
4187
  const claudeOauthToken = secretPatchValue(els.settingsClaudeOauth);
4420
4188
  if (claudeOauthToken !== undefined) patch.claudeOauthToken = claudeOauthToken;
4421
- const openrouterApiKey = secretPatchValue(els.settingsOpenrouterKey);
4422
- if (openrouterApiKey !== undefined) patch.openrouterApiKey = openrouterApiKey;
4423
4189
 
4424
4190
  if (ticketSource === 'jira' || els.settingsJiraBaseUrl?.value) {
4425
4191
  patch.jiraBaseUrl = els.settingsJiraBaseUrl?.value?.trim() || '';
@@ -4587,12 +4353,6 @@ bindSecretClear(
4587
4353
  els.settingsClaudeOauthHint,
4588
4354
  'Saved CLAUDE_CODE_OAUTH_TOKEN will be removed when you click Save settings.'
4589
4355
  );
4590
- bindSecretClear(
4591
- els.settingsOpenrouterKeyClear,
4592
- els.settingsOpenrouterKey,
4593
- els.settingsOpenrouterKeyHint,
4594
- 'Saved OPENROUTER_API_KEY will be removed when you click Save settings.'
4595
- );
4596
4356
 
4597
4357
 
4598
4358
  document.addEventListener('click', (ev) => {
@@ -4625,38 +4385,6 @@ document.addEventListener('keydown', (ev) => {
4625
4385
  }
4626
4386
  });
4627
4387
 
4628
- els.settingsLlmProvider?.addEventListener('change', () => {
4629
- const provider = currentLlmProviderSelection();
4630
- if (!isLlmProviderAvailable(provider)) {
4631
- const fallback =
4632
- appConfig.llmProvider === 'openrouter' ? 'openrouter' : 'claude';
4633
- syncLlmProviderSelects(fallback);
4634
- return;
4635
- }
4636
- for (const cb of modelComboboxes) {
4637
- setModelComboboxOpen(cb, false);
4638
- }
4639
- void onLlmProviderChange(provider);
4640
- });
4641
-
4642
- els.overviewLlmProvider?.addEventListener('change', () => {
4643
- const provider =
4644
- els.overviewLlmProvider.value === 'openrouter' ? 'openrouter' : 'claude';
4645
- if (!isLlmProviderAvailable(provider)) {
4646
- const fallback =
4647
- appConfig.llmProvider === 'openrouter' ? 'openrouter' : 'claude';
4648
- syncLlmProviderSelects(fallback);
4649
- return;
4650
- }
4651
- for (const cb of modelComboboxes) {
4652
- setModelComboboxOpen(cb, false);
4653
- }
4654
- for (const ap of agentPickers) {
4655
- setAgentPickerOpen(ap, false);
4656
- }
4657
- void onLlmProviderChange(provider);
4658
- });
4659
-
4660
4388
  if (
4661
4389
  els.settingsModelPicker &&
4662
4390
  els.settingsModelTrigger &&
@@ -4699,9 +4427,6 @@ if (
4699
4427
  list: els.overviewAgentModelList,
4700
4428
  valueEl: els.overviewAgentValue,
4701
4429
  iconEl: els.overviewAgentIcon,
4702
- tabs: [
4703
- ...(els.overviewAgentPanel.querySelectorAll('.agent-picker-tab') || []),
4704
- ],
4705
4430
  hiddenInput: els.overviewModel,
4706
4431
  optionIdPrefix: 'overview-agent-model-opt',
4707
4432
  onSelect: (modelId) => {
@@ -5031,7 +4756,9 @@ els.logJobFilter.addEventListener('change', () => {
5031
4756
  });
5032
4757
 
5033
4758
  applyTheme(loadTheme());
5034
- fetchConfig();
5035
- fetchModels();
4759
+ void (async () => {
4760
+ await fetchConfig();
4761
+ await fetchModels();
4762
+ })();
5036
4763
  fetchJobs();
5037
4764
  setInterval(fetchJobs, 3000);