acdev 1.0.8 → 1.0.10

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
@@ -129,51 +129,18 @@ let settingsSaving = false;
129
129
  let settingsTab = 'ticket';
130
130
  /** Sentinel: no model selected (jobs blocked until user picks one). */
131
131
  const NO_MODEL = '-';
132
- const OPENROUTER_AGENT_MODEL_PREFIX = 'anthropic/';
133
132
  /** @type {string} */
134
133
  let modelSourceHintBase = '';
135
- /**
136
- * @param {string} modelId
137
- * @returns {boolean}
138
- */
139
- function isOpenRouterAgentCompatibleModel(modelId) {
140
- if (!modelId || !String(modelId).includes('/')) return false;
141
- return String(modelId).trim().toLowerCase().startsWith(OPENROUTER_AGENT_MODEL_PREFIX);
142
- }
143
-
144
- /**
145
- * @param {HTMLElement} btn
146
- * @param {{ id: string, agentCompatible?: boolean }} model
147
- */
148
- function appendModelCompatibilityBadge(btn, model) {
149
- if (currentLlmProviderSelection() !== 'openrouter') return;
150
- const compatible =
151
- model.agentCompatible ?? isOpenRouterAgentCompatibleModel(model.id);
152
- const badge = document.createElement('span');
153
- badge.className = compatible
154
- ? 'model-picker-badge model-picker-badge--agent'
155
- : 'model-picker-badge model-picker-badge--reference';
156
- badge.textContent = compatible ? 'Agent' : 'May not work';
157
- badge.title = compatible
158
- ? 'Supported for acdev agent jobs (anthropic/* via Claude Agent SDK)'
159
- : 'Listed for reference; agent jobs require anthropic/* models';
160
- btn.appendChild(badge);
161
- }
162
134
 
163
135
  /**
164
136
  * @param {HTMLElement} btn
165
- * @param {{ id: string, label?: string, name?: string, agentCompatible?: boolean }} model
137
+ * @param {{ id: string, label?: string, name?: string }} model
166
138
  */
167
139
  function populateModelPickerOption(btn, model) {
168
- const titleRow = document.createElement('span');
169
- titleRow.className = 'model-picker-option-title-row';
170
-
171
140
  const title = document.createElement('span');
172
141
  title.className = 'model-picker-option-title';
173
142
  title.textContent = modelPickerLabel(model);
174
- titleRow.appendChild(title);
175
- appendModelCompatibilityBadge(titleRow, model);
176
- btn.appendChild(titleRow);
143
+ btn.appendChild(title);
177
144
 
178
145
  if (model.id !== modelPickerLabel(model)) {
179
146
  const sub = document.createElement('span');
@@ -185,23 +152,12 @@ function populateModelPickerOption(btn, model) {
185
152
 
186
153
  function refreshModelSourceHint() {
187
154
  if (!els.settingsModelHint) return;
188
- const provider = currentLlmProviderSelection();
189
- let text = modelSourceHintBase;
190
- if (
191
- provider === 'openrouter' &&
192
- !isNoModelSelection(currentModel) &&
193
- !isOpenRouterAgentCompatibleModel(currentModel)
194
- ) {
195
- text += ` Selected model "${currentModel}" may not work for agent jobs — choose an anthropic/* model to run jobs.`;
196
- }
197
- els.settingsModelHint.textContent = text;
155
+ els.settingsModelHint.textContent = modelSourceHintBase;
198
156
  }
199
157
  /** @type {string} */
200
158
  let currentModel = 'claude-sonnet-5';
201
159
  /** @type {Array<{id:string,label?:string,name?:string}>} */
202
160
  let availableModels = [];
203
- /** @type {'claude' | 'openrouter' | null} */
204
- let availableModelsProvider = null;
205
161
  /** @type {'github' | 'jira'} */
206
162
  let ticketSource = 'github';
207
163
  /** @type {{
@@ -234,11 +190,6 @@ let ticketSource = 'github';
234
190
  * anthropicApiKeyMasked?: string | null,
235
191
  * claudeOauthTokenSet?: boolean,
236
192
  * claudeOauthTokenMasked?: string | null,
237
- * llmProvider?: 'claude' | 'openrouter',
238
- * lastModelsByProvider?: Record<string, string>,
239
- * openrouterAuthOk?: boolean,
240
- * openrouterApiKeySet?: boolean,
241
- * openrouterApiKeyMasked?: string | null,
242
193
  * llmAuthOk?: boolean,
243
194
  * stubAgent?: boolean,
244
195
  * }} */
@@ -254,7 +205,6 @@ const els = {
254
205
  modelProvider: document.getElementById('model-provider'),
255
206
  modelCurrent: document.getElementById('model-current'),
256
207
  modelCombined: document.getElementById('model-combined'),
257
- overviewLlmProvider: document.getElementById('overview-llm-provider'),
258
208
  overviewModel: document.getElementById('overview-model'),
259
209
  overviewAgentPicker: document.getElementById('overview-agent-picker'),
260
210
  overviewAgentTrigger: document.getElementById('overview-agent-trigger'),
@@ -278,6 +228,7 @@ const els = {
278
228
  enqueueJiraSettingsLink: document.getElementById('enqueue-jira-settings-link'),
279
229
  overviewTicketSource: document.getElementById('overview-ticket-source'),
280
230
  enqueueFeedback: document.getElementById('enqueue-feedback'),
231
+ preferredBranch: document.getElementById('preferred-branch'),
281
232
  addBtn: document.getElementById('add-btn'),
282
233
  statsGrid: document.getElementById('stats-grid'),
283
234
  overviewRunning: document.getElementById('overview-running'),
@@ -340,8 +291,6 @@ const els = {
340
291
  settingsModelList: document.getElementById('settings-model-list'),
341
292
  settingsModelSearch: document.getElementById('settings-model-search'),
342
293
  settingsModelHint: document.getElementById('settings-model-hint'),
343
- settingsLlmProvider: document.getElementById('settings-llm-provider'),
344
- settingsLlmProviderHint: document.getElementById('settings-llm-provider-hint'),
345
294
  settingsMaxTurns: document.getElementById('settings-max-turns'),
346
295
  settingsTimeout: document.getElementById('settings-timeout'),
347
296
  settingsTimeoutMs: document.getElementById('settings-timeout-ms'),
@@ -382,10 +331,6 @@ const els = {
382
331
  settingsClaudeOauth: document.getElementById('settings-claude-oauth'),
383
332
  settingsClaudeOauthClear: document.getElementById('settings-claude-oauth-clear'),
384
333
  settingsClaudeOauthHint: document.getElementById('settings-claude-oauth-hint'),
385
- settingsOpenrouterStatus: document.getElementById('settings-openrouter-status'),
386
- settingsOpenrouterKey: document.getElementById('settings-openrouter-key'),
387
- settingsOpenrouterKeyClear: document.getElementById('settings-openrouter-key-clear'),
388
- settingsOpenrouterKeyHint: document.getElementById('settings-openrouter-key-hint'),
389
334
  settingsTabs: document.getElementById('settings-tabs'),
390
335
  };
391
336
 
@@ -956,16 +901,22 @@ function jobTitle(job) {
956
901
  return job.issueTitle || truncate(job.issueUrl, 48);
957
902
  }
958
903
 
904
+ function jobDisplayBranch(job) {
905
+ return job.branchName || job.preferredBranchName || '';
906
+ }
907
+
959
908
  function jobRepoLine(job) {
960
909
  if (isJiraJob(job)) {
961
910
  const key = parseJiraKeyFromJob(job) || '—';
962
- const branch = job.branchName ? ` · ${job.branchName}` : '';
911
+ const name = jobDisplayBranch(job);
912
+ const branch = name ? ` · ${name}` : '';
963
913
  return { repo: 'Jira', number: key, branch, text: `Jira ${key}${branch}`, isJira: true };
964
914
  }
965
915
  const ref = parseIssueRef(job.issueUrl);
966
916
  const repo = ref?.full || '—';
967
917
  const num = ref?.number || job.issueNumber || '—';
968
- const branch = job.branchName ? ` · ${job.branchName}` : '';
918
+ const name = jobDisplayBranch(job);
919
+ const branch = name ? ` · ${name}` : '';
969
920
  return { repo, number: num, branch, text: `${repo} #${num}${branch}`, isJira: false };
970
921
  }
971
922
 
@@ -2752,6 +2703,7 @@ function jobMatchesReviewSearch(job, q) {
2752
2703
  job.issueTitle,
2753
2704
  job.issueUrl,
2754
2705
  job.branchName,
2706
+ job.preferredBranchName,
2755
2707
  job.prTitle,
2756
2708
  job.prUrl,
2757
2709
  ref?.number,
@@ -3042,34 +2994,21 @@ async function readJson(res) {
3042
2994
  }
3043
2995
  }
3044
2996
 
3045
- function llmProviderLabel(provider) {
3046
- return provider === 'openrouter' ? 'OpenRouter' : 'Claude';
3047
- }
3048
-
3049
2997
  /**
3050
- * @param {'claude' | 'openrouter'} [provider]
3051
2998
  * @param {string} [modelId]
3052
2999
  * @returns {string}
3053
3000
  */
3054
- function agentDisplayText(provider = currentLlmProviderSelection(), modelId = currentModel) {
3055
- const providerName = llmProviderLabel(provider);
3056
- const modelText = isNoModelSelection(modelId) ? '—' : modelPickerDisplayText(modelId);
3057
- return `${providerName} • ${modelText}`;
3001
+ function agentDisplayText(modelId = currentModel) {
3002
+ return isNoModelSelection(modelId) ? '—' : modelPickerDisplayText(modelId);
3058
3003
  }
3059
3004
 
3060
3005
  function updateModelLabel() {
3061
- const provider = appConfig.llmProvider === 'openrouter' ? 'openrouter' : 'claude';
3062
- const providerName = llmProviderLabel(provider);
3063
3006
  const modelText = isNoModelSelection(currentModel) ? '—' : currentModel;
3064
3007
  const modelTitle = isNoModelSelection(currentModel)
3065
3008
  ? 'none selected'
3066
3009
  : currentModel;
3067
- const combined = agentDisplayText(provider, currentModel);
3010
+ const combined = agentDisplayText(currentModel);
3068
3011
 
3069
- if (els.modelProvider) {
3070
- els.modelProvider.textContent = providerName;
3071
- els.modelProvider.title = `${providerName} — switch in Overview or Settings`;
3072
- }
3073
3012
  if (els.modelCurrent) {
3074
3013
  els.modelCurrent.textContent = modelText;
3075
3014
  els.modelCurrent.title = `Model: ${modelTitle} — switch in Overview or Settings`;
@@ -3082,29 +3021,16 @@ function updateModelLabel() {
3082
3021
  refreshModelSourceHint();
3083
3022
  }
3084
3023
 
3085
- /**
3086
- * @param {'claude' | 'openrouter'} [provider]
3087
- * @returns {Array<{id:string,label?:string,name?:string}>}
3088
- */
3089
- function getAvailableModels(provider = currentLlmProviderSelection()) {
3090
- if (availableModelsProvider === provider && availableModels.length) {
3091
- return availableModels;
3092
- }
3093
- return defaultModelsForProvider(provider);
3024
+ function getAvailableModels() {
3025
+ return availableModels.length ? availableModels : defaultModels();
3094
3026
  }
3095
3027
 
3096
- /**
3097
- * @param {'claude' | 'openrouter'} provider
3098
- * @param {Array<{id:string,label?:string,name?:string}>} models
3099
- */
3100
- function setAvailableModels(provider, models) {
3101
- availableModelsProvider = provider;
3028
+ function setAvailableModels(models) {
3102
3029
  availableModels = models;
3103
3030
  }
3104
3031
 
3105
3032
  function clearAvailableModels() {
3106
3033
  availableModels = [];
3107
- availableModelsProvider = null;
3108
3034
  }
3109
3035
 
3110
3036
  /**
@@ -3391,76 +3317,8 @@ function renderAllModelComboboxLists(selectedId = currentModel) {
3391
3317
  /** @type {AgentPickerInstance[]} */
3392
3318
  const agentPickers = [];
3393
3319
 
3394
- const PROVIDER_UNAVAILABLE_TOOLTIP =
3395
- 'Not configured — add credentials in Settings → Authentication';
3396
-
3397
- /**
3398
- * @param {'claude' | 'openrouter'} provider
3399
- * @param {typeof appConfig} [cfg]
3400
- */
3401
- function isLlmProviderAvailable(provider, cfg = appConfig) {
3402
- if (cfg.stubAgent) return true;
3403
- if (provider === 'openrouter') return cfg.openrouterAuthOk === true;
3404
- return cfg.claudeAuthOk === true;
3405
- }
3406
-
3407
- /**
3408
- * Disable provider tabs and select options when auth is missing.
3409
- * @param {typeof appConfig} [cfg]
3410
- */
3411
- function syncLlmProviderAvailability(cfg = appConfig) {
3412
- for (const ap of agentPickers) {
3413
- for (const tab of ap.tabs) {
3414
- const provider =
3415
- tab.dataset.provider === 'openrouter' ? 'openrouter' : 'claude';
3416
- const available = isLlmProviderAvailable(provider, cfg);
3417
- tab.disabled = !available;
3418
- tab.classList.toggle('is-disabled', !available);
3419
- tab.setAttribute('aria-disabled', available ? 'false' : 'true');
3420
- if (available) {
3421
- tab.removeAttribute('title');
3422
- } else {
3423
- tab.title = PROVIDER_UNAVAILABLE_TOOLTIP;
3424
- }
3425
- }
3426
- }
3427
-
3428
- for (const select of [els.settingsLlmProvider, els.overviewLlmProvider]) {
3429
- if (!select) continue;
3430
- for (const opt of select.options) {
3431
- const provider = opt.value === 'openrouter' ? 'openrouter' : 'claude';
3432
- const available = isLlmProviderAvailable(provider, cfg);
3433
- opt.disabled = !available;
3434
- if (available) {
3435
- opt.removeAttribute('title');
3436
- } else {
3437
- opt.title = PROVIDER_UNAVAILABLE_TOOLTIP;
3438
- }
3439
- }
3440
- }
3441
- }
3442
-
3443
- /**
3444
- * @param {'claude' | 'openrouter'} provider
3445
- */
3446
- function syncAgentPickerTabs(provider) {
3447
- const value = provider === 'openrouter' ? 'openrouter' : 'claude';
3448
- for (const ap of agentPickers) {
3449
- for (const tab of ap.tabs) {
3450
- const active = tab.dataset.provider === value;
3451
- tab.classList.toggle('is-active', active);
3452
- tab.setAttribute('aria-selected', active ? 'true' : 'false');
3453
- }
3454
- if (ap.iconEl) {
3455
- ap.iconEl.classList.toggle('agent-pill-icon--claude', value === 'claude');
3456
- ap.iconEl.classList.toggle('agent-pill-icon--openrouter', value === 'openrouter');
3457
- }
3458
- }
3459
- }
3460
-
3461
3320
  function syncAgentPickerDisplay(selectedId = currentModel) {
3462
- const provider = currentLlmProviderSelection();
3463
- const text = agentDisplayText(provider, selectedId);
3321
+ const text = agentDisplayText(selectedId);
3464
3322
  for (const ap of agentPickers) {
3465
3323
  ap.valueEl.textContent = text;
3466
3324
  ap.valueEl.classList.toggle('is-empty', isNoModelSelection(selectedId));
@@ -3468,7 +3326,6 @@ function syncAgentPickerDisplay(selectedId = currentModel) {
3468
3326
  ap.hiddenInput.value = isNoModelSelection(selectedId) ? NO_MODEL : selectedId;
3469
3327
  }
3470
3328
  }
3471
- syncAgentPickerTabs(provider);
3472
3329
  }
3473
3330
 
3474
3331
  /**
@@ -3492,7 +3349,6 @@ function setAgentPickerOpen(instance, open) {
3492
3349
  if (open) {
3493
3350
  instance.activeIndex = -1;
3494
3351
  instance.search.value = '';
3495
- syncAgentPickerTabs(currentLlmProviderSelection());
3496
3352
  renderAgentPickerModelList(instance, currentModel);
3497
3353
  focusWithoutScroll(instance.search);
3498
3354
  } else {
@@ -3629,19 +3485,6 @@ function initAgentPicker(opts) {
3629
3485
  }
3630
3486
  });
3631
3487
 
3632
- for (const tab of instance.tabs) {
3633
- tab.addEventListener('click', () => {
3634
- if (tab.disabled) return;
3635
- const provider =
3636
- tab.dataset.provider === 'openrouter' ? 'openrouter' : 'claude';
3637
- if (!isLlmProviderAvailable(provider)) return;
3638
- if (provider === currentLlmProviderSelection()) return;
3639
- syncLlmProviderSelects(provider);
3640
- syncAgentPickerTabs(provider);
3641
- void onLlmProviderChange(provider);
3642
- });
3643
- }
3644
-
3645
3488
  return instance;
3646
3489
  }
3647
3490
 
@@ -3674,33 +3517,10 @@ function reconcileModelForProvider(models, candidate = currentModel, storedCandi
3674
3517
  return NO_MODEL;
3675
3518
  }
3676
3519
 
3677
- /**
3678
- * @param {'claude' | 'openrouter'} provider
3679
- * @returns {string | undefined}
3680
- */
3681
- function lastModelForProvider(provider) {
3682
- const stored = appConfig.lastModelsByProvider?.[provider];
3683
- return typeof stored === 'string' && !isNoModelSelection(stored) ? stored : undefined;
3684
- }
3685
-
3686
3520
  /**
3687
3521
  * @returns {Array<{id:string,label?:string,name?:string}>}
3688
3522
  */
3689
- function defaultModelsForProvider(provider = 'claude') {
3690
- if (provider === 'openrouter') {
3691
- return [
3692
- { id: 'anthropic/claude-sonnet-4', label: 'Claude Sonnet 4 (Anthropic)', agentCompatible: true },
3693
- { id: 'anthropic/claude-opus-4', label: 'Claude Opus 4 (Anthropic)', agentCompatible: true },
3694
- { id: 'anthropic/claude-3.5-sonnet', label: 'Claude 3.5 Sonnet (Anthropic)', agentCompatible: true },
3695
- { id: 'anthropic/claude-3.7-sonnet', label: 'Claude 3.7 Sonnet (Anthropic)', agentCompatible: true },
3696
- { id: 'openai/gpt-4o', label: 'GPT-4o (OpenAI)', agentCompatible: false },
3697
- { id: 'openai/gpt-4o-mini', label: 'GPT-4o Mini (OpenAI)', agentCompatible: false },
3698
- { id: 'google/gemini-2.5-pro-preview', label: 'Gemini 2.5 Pro Preview (Google)', agentCompatible: false },
3699
- { id: 'qwen/qwen3-235b-a22b', label: 'Qwen3 235B (Qwen)', agentCompatible: false },
3700
- { id: 'deepseek/deepseek-chat-v3-0324', label: 'DeepSeek Chat V3 (DeepSeek)', agentCompatible: false },
3701
- { id: 'meta-llama/llama-3.3-70b-instruct', label: 'Llama 3.3 70B Instruct (Meta)', agentCompatible: false },
3702
- ];
3703
- }
3523
+ function defaultModels() {
3704
3524
  return [
3705
3525
  { id: 'claude-sonnet-5', label: 'Sonnet 5' },
3706
3526
  { id: 'claude-opus-5', label: 'Opus 5' },
@@ -3712,28 +3532,18 @@ function defaultModelsForProvider(provider = 'claude') {
3712
3532
  }
3713
3533
 
3714
3534
  /**
3715
- * @param {'anthropic' | 'openrouter' | 'fallback' | string | undefined} source
3716
- * @param {'claude' | 'openrouter'} [provider]
3535
+ * @param {'anthropic' | 'fallback' | string | undefined} source
3717
3536
  */
3718
- function updateModelSourceHint(source, provider = 'claude') {
3537
+ function updateModelSourceHint(source) {
3719
3538
  if (!els.settingsModelHint) return;
3720
- const name = provider === 'openrouter' ? 'OpenRouter' : 'Claude';
3721
3539
  if (source === 'anthropic') {
3722
3540
  modelSourceHintBase =
3723
- `${name} model for agent runs. List loaded from Anthropic Models API.`;
3724
- } else if (source === 'openrouter') {
3725
- modelSourceHintBase =
3726
- 'OpenRouter models for agent runs. Full catalog from OpenRouter Models API. Agent jobs require anthropic/* models (marked “Agent” in the list).';
3541
+ 'Claude model for agent runs. List loaded from Anthropic Models API.';
3727
3542
  } else if (source === 'fallback') {
3728
- if (provider === 'openrouter') {
3729
- modelSourceHintBase =
3730
- 'OpenRouter models for agent runs. Showing curated catalog (live list unavailable — set OPENROUTER_API_KEY in Authentication). Agent jobs require anthropic/* models.';
3731
- } else {
3732
- modelSourceHintBase =
3733
- 'Claude model for agent runs. Showing curated Claude Code models (live list unavailable — set an API key or use claude auth login).';
3734
- }
3543
+ modelSourceHintBase =
3544
+ 'Claude model for agent runs. Showing curated Claude Code models (live list unavailable — set an API key or use claude auth login).';
3735
3545
  } else {
3736
- modelSourceHintBase = `${name} model for agent runs.`;
3546
+ modelSourceHintBase = 'Claude model for agent runs.';
3737
3547
  }
3738
3548
  refreshModelSourceHint();
3739
3549
  }
@@ -3759,92 +3569,55 @@ function modelMatchesSearchQuery(model, query) {
3759
3569
  .some((part) => String(part).toLowerCase().includes(q));
3760
3570
  }
3761
3571
 
3762
- /**
3763
- * @returns {'claude' | 'openrouter'}
3764
- */
3765
- function currentLlmProviderSelection() {
3766
- const fromOverview = els.overviewLlmProvider?.value;
3767
- if (fromOverview === 'openrouter' || fromOverview === 'claude') return fromOverview;
3768
- const fromForm = els.settingsLlmProvider?.value;
3769
- if (fromForm === 'openrouter' || fromForm === 'claude') return fromForm;
3770
- return appConfig.llmProvider === 'openrouter' ? 'openrouter' : 'claude';
3771
- }
3772
-
3773
- /**
3774
- * @param {'claude' | 'openrouter'} provider
3775
- */
3776
- function syncLlmProviderSelects(provider) {
3777
- const value = provider === 'openrouter' ? 'openrouter' : 'claude';
3778
- if (els.settingsLlmProvider) els.settingsLlmProvider.value = value;
3779
- if (els.overviewLlmProvider) els.overviewLlmProvider.value = value;
3780
- syncAgentPickerTabs(value);
3781
- if (els.settingsLlmProviderHint) {
3782
- els.settingsLlmProviderHint.textContent =
3783
- value === 'openrouter'
3784
- ? 'Agent runs route through OpenRouter (requires OPENROUTER_API_KEY). All models are listed; agent jobs need anthropic/* models.'
3785
- : 'Agent runs use Claude via Anthropic API, OAuth token, or claude auth login.';
3786
- }
3787
- }
3788
-
3789
- /**
3790
- * @param {{ models?: Array<{id:string,label?:string,name?:string}>, selected?: string, source?: string, provider?: string }} data
3791
- */
3792
3572
  function populateSettingsModels(data) {
3793
- const provider = data?.provider || currentLlmProviderSelection();
3794
- const cached = getAvailableModels(provider);
3795
3573
  let models = Array.isArray(data?.models)
3796
3574
  ? data.models
3797
- : data?.reconcileProvider
3575
+ : data?.reconcile
3798
3576
  ? []
3799
- : cached.length
3800
- ? cached
3801
- : defaultModelsForProvider(provider);
3577
+ : getAvailableModels().length
3578
+ ? getAvailableModels()
3579
+ : defaultModels();
3802
3580
  if (!models.length && data?.source) {
3803
- models = defaultModelsForProvider(provider);
3581
+ models = defaultModels();
3804
3582
  }
3805
- setAvailableModels(provider, models);
3583
+ setAvailableModels(models);
3806
3584
 
3807
- if (data?.reconcileProvider) {
3585
+ if (data?.reconcile) {
3808
3586
  currentModel = reconcileModelForProvider(
3809
3587
  models,
3810
- data.reconcileCandidate,
3811
- lastModelForProvider(provider)
3588
+ data.reconcileCandidate
3812
3589
  );
3813
3590
  } else if (data?.selected != null) {
3814
3591
  const next = isNoModelSelection(data.selected) ? NO_MODEL : data.selected;
3815
- currentModel = reconcileModelForProvider(models, next, lastModelForProvider(provider));
3592
+ currentModel = reconcileModelForProvider(models, next);
3816
3593
  }
3817
- if (data?.source) updateModelSourceHint(data.source, provider);
3594
+ if (data?.source) updateModelSourceHint(data.source);
3818
3595
 
3819
3596
  syncAllModelComboboxValues(currentModel);
3820
3597
  renderAllModelComboboxLists(currentModel);
3821
3598
  updateModelLabel();
3822
3599
  }
3823
3600
 
3824
- function modelsForFetchFallback(provider) {
3825
- const cached = getAvailableModels(provider);
3826
- if (availableModelsProvider === provider && cached.length) return cached;
3827
- return defaultModelsForProvider(provider);
3601
+ function modelsForFetchFallback() {
3602
+ return getAvailableModels().length ? getAvailableModels() : defaultModels();
3828
3603
  }
3829
3604
 
3830
3605
  async function fetchModels(opts = {}) {
3831
3606
  const refresh = opts.refresh === true;
3832
- const provider = opts.provider || currentLlmProviderSelection();
3833
- const reconcileProvider = opts.reconcileProvider === true;
3607
+ const reconcile = opts.reconcile === true;
3834
3608
  const reconcileCandidate = opts.reconcileCandidate;
3835
- if (reconcileProvider) {
3609
+ if (reconcile) {
3836
3610
  clearAvailableModels();
3837
3611
  }
3838
- const query = new URLSearchParams({ provider });
3612
+ const query = new URLSearchParams();
3839
3613
  if (refresh) query.set('refresh', '1');
3840
3614
  try {
3841
3615
  const res = await fetch(`/api/models?${query.toString()}`);
3842
3616
  if (!res.ok) {
3843
3617
  populateSettingsModels({
3844
- models: modelsForFetchFallback(provider),
3618
+ models: modelsForFetchFallback(),
3845
3619
  selected: currentModel,
3846
- provider,
3847
- reconcileProvider,
3620
+ reconcile,
3848
3621
  reconcileCandidate,
3849
3622
  });
3850
3623
  return;
@@ -3852,17 +3625,15 @@ async function fetchModels(opts = {}) {
3852
3625
  const data = await readJson(res);
3853
3626
  populateSettingsModels({
3854
3627
  ...data,
3855
- provider: data.provider || provider,
3856
- reconcileProvider,
3628
+ reconcile,
3857
3629
  reconcileCandidate,
3858
3630
  });
3859
3631
  } catch (err) {
3860
3632
  console.error('Failed to fetch models:', err);
3861
3633
  populateSettingsModels({
3862
- models: modelsForFetchFallback(provider),
3634
+ models: modelsForFetchFallback(),
3863
3635
  selected: currentModel,
3864
- provider,
3865
- reconcileProvider,
3636
+ reconcile,
3866
3637
  reconcileCandidate,
3867
3638
  });
3868
3639
  }
@@ -3982,13 +3753,6 @@ function claudeStatusText(cfg) {
3982
3753
  }
3983
3754
  }
3984
3755
 
3985
- function openrouterStatusText(cfg) {
3986
- if (cfg.openrouterAuthOk) {
3987
- return 'Authenticated via OPENROUTER_API_KEY';
3988
- }
3989
- return 'Not authenticated — add an OpenRouter API key below';
3990
- }
3991
-
3992
3756
  function fillAuthSettings(cfg) {
3993
3757
  if (els.settingsGhStatus) {
3994
3758
  els.settingsGhStatus.textContent = ghStatusText(cfg);
@@ -4047,25 +3811,6 @@ function fillAuthSettings(cfg) {
4047
3811
  ? '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.'
4048
3812
  : '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.';
4049
3813
  }
4050
-
4051
- if (els.settingsOpenrouterStatus) {
4052
- els.settingsOpenrouterStatus.textContent = openrouterStatusText(cfg);
4053
- els.settingsOpenrouterStatus.className = cfg.openrouterAuthOk
4054
- ? 'auth-status ok'
4055
- : 'auth-status err';
4056
- }
4057
- fillSecretInput(
4058
- els.settingsOpenrouterKey,
4059
- els.settingsOpenrouterKeyClear,
4060
- Boolean(cfg.openrouterApiKeySet),
4061
- cfg.openrouterApiKeyMasked,
4062
- 'Paste an OpenRouter API key'
4063
- );
4064
- if (els.settingsOpenrouterKeyHint) {
4065
- els.settingsOpenrouterKeyHint.innerHTML = cfg.openrouterApiKeySet
4066
- ? 'API key stored in <code>.acdev/.env</code> as <code>OPENROUTER_API_KEY</code> (not committed).'
4067
- : '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>.';
4068
- }
4069
3814
  }
4070
3815
 
4071
3816
  function fillSettingsForm(cfg) {
@@ -4145,23 +3890,17 @@ function fillSettingsForm(cfg) {
4145
3890
  els.settingsBaseBranch.value = cfg.baseBranch || '';
4146
3891
  }
4147
3892
 
4148
- if (els.settingsLlmProvider) {
4149
- syncLlmProviderSelects(cfg.llmProvider === 'openrouter' ? 'openrouter' : 'claude');
4150
- }
4151
-
4152
3893
  if (els.settingsModelPicker) {
4153
- const provider = cfg.llmProvider === 'openrouter' ? 'openrouter' : 'claude';
4154
- if (getAvailableModels(provider).length) {
3894
+ if (getAvailableModels().length) {
4155
3895
  populateSettingsModels({
4156
- models: getAvailableModels(provider),
3896
+ models: getAvailableModels(),
4157
3897
  selected: cfg.model ?? currentModel,
4158
- provider,
4159
3898
  });
4160
3899
  } else if (cfg.model != null) {
4161
3900
  currentModel = isNoModelSelection(cfg.model) ? NO_MODEL : cfg.model;
4162
3901
  syncModelPickerValue(currentModel);
4163
3902
  }
4164
- void fetchModels({ provider });
3903
+ void fetchModels();
4165
3904
  }
4166
3905
 
4167
3906
  if (els.settingsMaxTurns) {
@@ -4354,16 +4093,14 @@ function setOverviewLlmFeedback(message, kind = 'ok') {
4354
4093
  */
4355
4094
  function fillOverviewLlmControls(cfg) {
4356
4095
  if (!cfg) return;
4357
- const provider = cfg.llmProvider === 'openrouter' ? 'openrouter' : 'claude';
4358
- syncLlmProviderSelects(provider);
4359
4096
  syncAllModelComboboxValues(currentModel);
4360
4097
  }
4361
4098
 
4362
4099
  let llmConfigSaving = false;
4363
4100
 
4364
4101
  /**
4365
- * PATCH llmProvider and/or model immediately (Overview switcher).
4366
- * @param {{ llmProvider?: 'claude' | 'openrouter', model?: string }} patch
4102
+ * PATCH model immediately (Overview switcher).
4103
+ * @param {{ model?: string }} patch
4367
4104
  */
4368
4105
  async function patchLlmConfig(patch) {
4369
4106
  if (llmConfigSaving) return;
@@ -4385,7 +4122,7 @@ async function patchLlmConfig(patch) {
4385
4122
  if (currentView === 'settings') {
4386
4123
  fillSettingsForm(data);
4387
4124
  }
4388
- setOverviewLlmFeedback('LLM updated. Next jobs will use these values.', 'ok');
4125
+ setOverviewLlmFeedback('Model updated. Next jobs will use this value.', 'ok');
4389
4126
  window.setTimeout(() => setOverviewLlmFeedback(null), 2500);
4390
4127
  } catch (err) {
4391
4128
  setOverviewLlmFeedback(err.message || 'Update failed', 'error');
@@ -4394,29 +4131,6 @@ async function patchLlmConfig(patch) {
4394
4131
  }
4395
4132
  }
4396
4133
 
4397
- /**
4398
- * @param {'claude' | 'openrouter'} provider
4399
- */
4400
- async function onLlmProviderChange(provider) {
4401
- const previousModel = currentModel;
4402
- syncLlmProviderSelects(provider);
4403
- clearAvailableModels();
4404
- currentModel = NO_MODEL;
4405
- syncAllModelComboboxValues(currentModel);
4406
- renderAllModelComboboxLists(currentModel);
4407
- updateModelLabel();
4408
- await fetchModels({
4409
- refresh: true,
4410
- provider,
4411
- reconcileProvider: true,
4412
- reconcileCandidate: previousModel,
4413
- });
4414
- await patchLlmConfig({
4415
- llmProvider: provider,
4416
- model: currentModel,
4417
- });
4418
- }
4419
-
4420
4134
  function applyConfigSnapshot(data) {
4421
4135
  appConfig = data || {};
4422
4136
  if (data?.model != null) {
@@ -4432,7 +4146,6 @@ function applyConfigSnapshot(data) {
4432
4146
  if (els.repoBranch) {
4433
4147
  els.repoBranch.textContent = data?.baseBranch || '—';
4434
4148
  }
4435
- syncLlmProviderAvailability(appConfig);
4436
4149
  if (currentView === 'settings') {
4437
4150
  fillSettingsForm(appConfig);
4438
4151
  }
@@ -4468,7 +4181,6 @@ function readSettingsForm() {
4468
4181
  const patch = {
4469
4182
  baseBranch,
4470
4183
  model,
4471
- llmProvider: els.settingsLlmProvider?.value === 'openrouter' ? 'openrouter' : 'claude',
4472
4184
  maxAgentTurns,
4473
4185
  agentTimeoutMs: Math.round(timeoutMinutes * 60_000),
4474
4186
  testCommand: testTrimmed === '' ? null : testTrimmed,
@@ -4482,8 +4194,6 @@ function readSettingsForm() {
4482
4194
  if (anthropicApiKey !== undefined) patch.anthropicApiKey = anthropicApiKey;
4483
4195
  const claudeOauthToken = secretPatchValue(els.settingsClaudeOauth);
4484
4196
  if (claudeOauthToken !== undefined) patch.claudeOauthToken = claudeOauthToken;
4485
- const openrouterApiKey = secretPatchValue(els.settingsOpenrouterKey);
4486
- if (openrouterApiKey !== undefined) patch.openrouterApiKey = openrouterApiKey;
4487
4197
 
4488
4198
  if (ticketSource === 'jira' || els.settingsJiraBaseUrl?.value) {
4489
4199
  patch.jiraBaseUrl = els.settingsJiraBaseUrl?.value?.trim() || '';
@@ -4651,12 +4361,6 @@ bindSecretClear(
4651
4361
  els.settingsClaudeOauthHint,
4652
4362
  'Saved CLAUDE_CODE_OAUTH_TOKEN will be removed when you click Save settings.'
4653
4363
  );
4654
- bindSecretClear(
4655
- els.settingsOpenrouterKeyClear,
4656
- els.settingsOpenrouterKey,
4657
- els.settingsOpenrouterKeyHint,
4658
- 'Saved OPENROUTER_API_KEY will be removed when you click Save settings.'
4659
- );
4660
4364
 
4661
4365
 
4662
4366
  document.addEventListener('click', (ev) => {
@@ -4689,38 +4393,6 @@ document.addEventListener('keydown', (ev) => {
4689
4393
  }
4690
4394
  });
4691
4395
 
4692
- els.settingsLlmProvider?.addEventListener('change', () => {
4693
- const provider = currentLlmProviderSelection();
4694
- if (!isLlmProviderAvailable(provider)) {
4695
- const fallback =
4696
- appConfig.llmProvider === 'openrouter' ? 'openrouter' : 'claude';
4697
- syncLlmProviderSelects(fallback);
4698
- return;
4699
- }
4700
- for (const cb of modelComboboxes) {
4701
- setModelComboboxOpen(cb, false);
4702
- }
4703
- void onLlmProviderChange(provider);
4704
- });
4705
-
4706
- els.overviewLlmProvider?.addEventListener('change', () => {
4707
- const provider =
4708
- els.overviewLlmProvider.value === 'openrouter' ? 'openrouter' : 'claude';
4709
- if (!isLlmProviderAvailable(provider)) {
4710
- const fallback =
4711
- appConfig.llmProvider === 'openrouter' ? 'openrouter' : 'claude';
4712
- syncLlmProviderSelects(fallback);
4713
- return;
4714
- }
4715
- for (const cb of modelComboboxes) {
4716
- setModelComboboxOpen(cb, false);
4717
- }
4718
- for (const ap of agentPickers) {
4719
- setAgentPickerOpen(ap, false);
4720
- }
4721
- void onLlmProviderChange(provider);
4722
- });
4723
-
4724
4396
  if (
4725
4397
  els.settingsModelPicker &&
4726
4398
  els.settingsModelTrigger &&
@@ -4763,9 +4435,6 @@ if (
4763
4435
  list: els.overviewAgentModelList,
4764
4436
  valueEl: els.overviewAgentValue,
4765
4437
  iconEl: els.overviewAgentIcon,
4766
- tabs: [
4767
- ...(els.overviewAgentPanel.querySelectorAll('.agent-picker-tab') || []),
4768
- ],
4769
4438
  hiddenInput: els.overviewModel,
4770
4439
  optionIdPrefix: 'overview-agent-model-opt',
4771
4440
  onSelect: (modelId) => {
@@ -4784,18 +4453,32 @@ els.issueUrls?.addEventListener('keydown', (ev) => {
4784
4453
  els.addBtn?.click();
4785
4454
  });
4786
4455
 
4456
+ els.preferredBranch?.addEventListener('keydown', (ev) => {
4457
+ if (ev.key !== 'Enter' || ev.shiftKey || ev.metaKey || ev.ctrlKey || ev.altKey) return;
4458
+ ev.preventDefault();
4459
+ els.addBtn?.click();
4460
+ });
4461
+
4787
4462
  els.addBtn.addEventListener('click', async () => {
4788
4463
  els.enqueueFeedback.classList.add('hidden');
4789
4464
  const text = els.issueUrls.value.trim();
4790
4465
  if (!text) return;
4791
4466
 
4792
4467
  const urls = splitIssueUrls(text);
4468
+ const branchName = els.preferredBranch?.value?.trim() || '';
4469
+ // #region agent log
4470
+ fetch('http://127.0.0.1:7258/ingest/377aa5e2-15ea-4447-a68b-7ce215882bc3',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'473a78'},body:JSON.stringify({sessionId:'473a78',runId:'pre-fix',hypothesisId:'B',location:'public/app.js:addBtn',message:'enqueue submit',data:{hasCustom:Boolean(branchName),branchName:branchName||null,urlCount:urls.length},timestamp:Date.now()})}).catch(()=>{});
4471
+ // #endregion
4793
4472
 
4794
4473
  try {
4795
4474
  const res = await fetch('/api/issues', {
4796
4475
  method: 'POST',
4797
4476
  headers: { 'Content-Type': 'application/json' },
4798
- body: JSON.stringify({ urls, ticketSource }),
4477
+ body: JSON.stringify({
4478
+ urls,
4479
+ ticketSource,
4480
+ ...(branchName ? { branchName } : {}),
4481
+ }),
4799
4482
  });
4800
4483
  const data = await res.json();
4801
4484
  if (!res.ok) {
@@ -4806,6 +4489,7 @@ els.addBtn.addEventListener('click', async () => {
4806
4489
  return;
4807
4490
  }
4808
4491
  els.issueUrls.value = '';
4492
+ if (els.preferredBranch) els.preferredBranch.value = '';
4809
4493
  const parts = [];
4810
4494
  if (data.jobs?.length) parts.push(`Added ${data.jobs.length}`);
4811
4495
  if (data.skipped?.length) {
@@ -5096,9 +4780,8 @@ els.logJobFilter.addEventListener('change', () => {
5096
4780
 
5097
4781
  applyTheme(loadTheme());
5098
4782
  void (async () => {
5099
- const cfg = await fetchConfig();
5100
- const provider = cfg?.llmProvider === 'openrouter' ? 'openrouter' : 'claude';
5101
- await fetchModels({ provider });
4783
+ await fetchConfig();
4784
+ await fetchModels();
5102
4785
  })();
5103
4786
  fetchJobs();
5104
4787
  setInterval(fetchJobs, 3000);