acdev 1.0.8 → 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/.acdev/.env.example +0 -3
- package/README.md +11 -21
- package/bin/acdev.js +3 -13
- package/package.json +1 -2
- package/public/app.js +45 -385
- package/public/index.html +1 -33
- package/public/styles.css +0 -31
- package/src/agent.js +64 -127
- package/src/config.js +45 -151
- package/src/models.js +35 -256
- package/src/server.js +11 -50
- package/src/openrouter-auth.js +0 -37
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
134
|
|
|
144
135
|
/**
|
|
145
136
|
* @param {HTMLElement} btn
|
|
146
|
-
* @param {{ id: string,
|
|
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
|
-
|
|
163
|
-
/**
|
|
164
|
-
* @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
|
-
|
|
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
|
-
|
|
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'),
|
|
@@ -340,8 +290,6 @@ const els = {
|
|
|
340
290
|
settingsModelList: document.getElementById('settings-model-list'),
|
|
341
291
|
settingsModelSearch: document.getElementById('settings-model-search'),
|
|
342
292
|
settingsModelHint: document.getElementById('settings-model-hint'),
|
|
343
|
-
settingsLlmProvider: document.getElementById('settings-llm-provider'),
|
|
344
|
-
settingsLlmProviderHint: document.getElementById('settings-llm-provider-hint'),
|
|
345
293
|
settingsMaxTurns: document.getElementById('settings-max-turns'),
|
|
346
294
|
settingsTimeout: document.getElementById('settings-timeout'),
|
|
347
295
|
settingsTimeoutMs: document.getElementById('settings-timeout-ms'),
|
|
@@ -382,10 +330,6 @@ const els = {
|
|
|
382
330
|
settingsClaudeOauth: document.getElementById('settings-claude-oauth'),
|
|
383
331
|
settingsClaudeOauthClear: document.getElementById('settings-claude-oauth-clear'),
|
|
384
332
|
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
333
|
settingsTabs: document.getElementById('settings-tabs'),
|
|
390
334
|
};
|
|
391
335
|
|
|
@@ -3042,34 +2986,21 @@ async function readJson(res) {
|
|
|
3042
2986
|
}
|
|
3043
2987
|
}
|
|
3044
2988
|
|
|
3045
|
-
function llmProviderLabel(provider) {
|
|
3046
|
-
return provider === 'openrouter' ? 'OpenRouter' : 'Claude';
|
|
3047
|
-
}
|
|
3048
|
-
|
|
3049
2989
|
/**
|
|
3050
|
-
* @param {'claude' | 'openrouter'} [provider]
|
|
3051
2990
|
* @param {string} [modelId]
|
|
3052
2991
|
* @returns {string}
|
|
3053
2992
|
*/
|
|
3054
|
-
function agentDisplayText(
|
|
3055
|
-
|
|
3056
|
-
const modelText = isNoModelSelection(modelId) ? '—' : modelPickerDisplayText(modelId);
|
|
3057
|
-
return `${providerName} • ${modelText}`;
|
|
2993
|
+
function agentDisplayText(modelId = currentModel) {
|
|
2994
|
+
return isNoModelSelection(modelId) ? '—' : modelPickerDisplayText(modelId);
|
|
3058
2995
|
}
|
|
3059
2996
|
|
|
3060
2997
|
function updateModelLabel() {
|
|
3061
|
-
const provider = appConfig.llmProvider === 'openrouter' ? 'openrouter' : 'claude';
|
|
3062
|
-
const providerName = llmProviderLabel(provider);
|
|
3063
2998
|
const modelText = isNoModelSelection(currentModel) ? '—' : currentModel;
|
|
3064
2999
|
const modelTitle = isNoModelSelection(currentModel)
|
|
3065
3000
|
? 'none selected'
|
|
3066
3001
|
: currentModel;
|
|
3067
|
-
const combined = agentDisplayText(
|
|
3002
|
+
const combined = agentDisplayText(currentModel);
|
|
3068
3003
|
|
|
3069
|
-
if (els.modelProvider) {
|
|
3070
|
-
els.modelProvider.textContent = providerName;
|
|
3071
|
-
els.modelProvider.title = `${providerName} — switch in Overview or Settings`;
|
|
3072
|
-
}
|
|
3073
3004
|
if (els.modelCurrent) {
|
|
3074
3005
|
els.modelCurrent.textContent = modelText;
|
|
3075
3006
|
els.modelCurrent.title = `Model: ${modelTitle} — switch in Overview or Settings`;
|
|
@@ -3082,29 +3013,16 @@ function updateModelLabel() {
|
|
|
3082
3013
|
refreshModelSourceHint();
|
|
3083
3014
|
}
|
|
3084
3015
|
|
|
3085
|
-
|
|
3086
|
-
|
|
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);
|
|
3016
|
+
function getAvailableModels() {
|
|
3017
|
+
return availableModels.length ? availableModels : defaultModels();
|
|
3094
3018
|
}
|
|
3095
3019
|
|
|
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;
|
|
3020
|
+
function setAvailableModels(models) {
|
|
3102
3021
|
availableModels = models;
|
|
3103
3022
|
}
|
|
3104
3023
|
|
|
3105
3024
|
function clearAvailableModels() {
|
|
3106
3025
|
availableModels = [];
|
|
3107
|
-
availableModelsProvider = null;
|
|
3108
3026
|
}
|
|
3109
3027
|
|
|
3110
3028
|
/**
|
|
@@ -3391,76 +3309,8 @@ function renderAllModelComboboxLists(selectedId = currentModel) {
|
|
|
3391
3309
|
/** @type {AgentPickerInstance[]} */
|
|
3392
3310
|
const agentPickers = [];
|
|
3393
3311
|
|
|
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
3312
|
function syncAgentPickerDisplay(selectedId = currentModel) {
|
|
3462
|
-
const
|
|
3463
|
-
const text = agentDisplayText(provider, selectedId);
|
|
3313
|
+
const text = agentDisplayText(selectedId);
|
|
3464
3314
|
for (const ap of agentPickers) {
|
|
3465
3315
|
ap.valueEl.textContent = text;
|
|
3466
3316
|
ap.valueEl.classList.toggle('is-empty', isNoModelSelection(selectedId));
|
|
@@ -3468,7 +3318,6 @@ function syncAgentPickerDisplay(selectedId = currentModel) {
|
|
|
3468
3318
|
ap.hiddenInput.value = isNoModelSelection(selectedId) ? NO_MODEL : selectedId;
|
|
3469
3319
|
}
|
|
3470
3320
|
}
|
|
3471
|
-
syncAgentPickerTabs(provider);
|
|
3472
3321
|
}
|
|
3473
3322
|
|
|
3474
3323
|
/**
|
|
@@ -3492,7 +3341,6 @@ function setAgentPickerOpen(instance, open) {
|
|
|
3492
3341
|
if (open) {
|
|
3493
3342
|
instance.activeIndex = -1;
|
|
3494
3343
|
instance.search.value = '';
|
|
3495
|
-
syncAgentPickerTabs(currentLlmProviderSelection());
|
|
3496
3344
|
renderAgentPickerModelList(instance, currentModel);
|
|
3497
3345
|
focusWithoutScroll(instance.search);
|
|
3498
3346
|
} else {
|
|
@@ -3629,19 +3477,6 @@ function initAgentPicker(opts) {
|
|
|
3629
3477
|
}
|
|
3630
3478
|
});
|
|
3631
3479
|
|
|
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
3480
|
return instance;
|
|
3646
3481
|
}
|
|
3647
3482
|
|
|
@@ -3674,33 +3509,10 @@ function reconcileModelForProvider(models, candidate = currentModel, storedCandi
|
|
|
3674
3509
|
return NO_MODEL;
|
|
3675
3510
|
}
|
|
3676
3511
|
|
|
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
3512
|
/**
|
|
3687
3513
|
* @returns {Array<{id:string,label?:string,name?:string}>}
|
|
3688
3514
|
*/
|
|
3689
|
-
function
|
|
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
|
-
}
|
|
3515
|
+
function defaultModels() {
|
|
3704
3516
|
return [
|
|
3705
3517
|
{ id: 'claude-sonnet-5', label: 'Sonnet 5' },
|
|
3706
3518
|
{ id: 'claude-opus-5', label: 'Opus 5' },
|
|
@@ -3712,28 +3524,18 @@ function defaultModelsForProvider(provider = 'claude') {
|
|
|
3712
3524
|
}
|
|
3713
3525
|
|
|
3714
3526
|
/**
|
|
3715
|
-
* @param {'anthropic' | '
|
|
3716
|
-
* @param {'claude' | 'openrouter'} [provider]
|
|
3527
|
+
* @param {'anthropic' | 'fallback' | string | undefined} source
|
|
3717
3528
|
*/
|
|
3718
|
-
function updateModelSourceHint(source
|
|
3529
|
+
function updateModelSourceHint(source) {
|
|
3719
3530
|
if (!els.settingsModelHint) return;
|
|
3720
|
-
const name = provider === 'openrouter' ? 'OpenRouter' : 'Claude';
|
|
3721
3531
|
if (source === 'anthropic') {
|
|
3722
3532
|
modelSourceHintBase =
|
|
3723
|
-
|
|
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).';
|
|
3533
|
+
'Claude model for agent runs. List loaded from Anthropic Models API.';
|
|
3727
3534
|
} else if (source === 'fallback') {
|
|
3728
|
-
|
|
3729
|
-
|
|
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
|
-
}
|
|
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).';
|
|
3735
3537
|
} else {
|
|
3736
|
-
modelSourceHintBase =
|
|
3538
|
+
modelSourceHintBase = 'Claude model for agent runs.';
|
|
3737
3539
|
}
|
|
3738
3540
|
refreshModelSourceHint();
|
|
3739
3541
|
}
|
|
@@ -3759,92 +3561,55 @@ function modelMatchesSearchQuery(model, query) {
|
|
|
3759
3561
|
.some((part) => String(part).toLowerCase().includes(q));
|
|
3760
3562
|
}
|
|
3761
3563
|
|
|
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
3564
|
function populateSettingsModels(data) {
|
|
3793
|
-
const provider = data?.provider || currentLlmProviderSelection();
|
|
3794
|
-
const cached = getAvailableModels(provider);
|
|
3795
3565
|
let models = Array.isArray(data?.models)
|
|
3796
3566
|
? data.models
|
|
3797
|
-
: data?.
|
|
3567
|
+
: data?.reconcile
|
|
3798
3568
|
? []
|
|
3799
|
-
:
|
|
3800
|
-
?
|
|
3801
|
-
:
|
|
3569
|
+
: getAvailableModels().length
|
|
3570
|
+
? getAvailableModels()
|
|
3571
|
+
: defaultModels();
|
|
3802
3572
|
if (!models.length && data?.source) {
|
|
3803
|
-
models =
|
|
3573
|
+
models = defaultModels();
|
|
3804
3574
|
}
|
|
3805
|
-
setAvailableModels(
|
|
3575
|
+
setAvailableModels(models);
|
|
3806
3576
|
|
|
3807
|
-
if (data?.
|
|
3577
|
+
if (data?.reconcile) {
|
|
3808
3578
|
currentModel = reconcileModelForProvider(
|
|
3809
3579
|
models,
|
|
3810
|
-
data.reconcileCandidate
|
|
3811
|
-
lastModelForProvider(provider)
|
|
3580
|
+
data.reconcileCandidate
|
|
3812
3581
|
);
|
|
3813
3582
|
} else if (data?.selected != null) {
|
|
3814
3583
|
const next = isNoModelSelection(data.selected) ? NO_MODEL : data.selected;
|
|
3815
|
-
currentModel = reconcileModelForProvider(models, next
|
|
3584
|
+
currentModel = reconcileModelForProvider(models, next);
|
|
3816
3585
|
}
|
|
3817
|
-
if (data?.source) updateModelSourceHint(data.source
|
|
3586
|
+
if (data?.source) updateModelSourceHint(data.source);
|
|
3818
3587
|
|
|
3819
3588
|
syncAllModelComboboxValues(currentModel);
|
|
3820
3589
|
renderAllModelComboboxLists(currentModel);
|
|
3821
3590
|
updateModelLabel();
|
|
3822
3591
|
}
|
|
3823
3592
|
|
|
3824
|
-
function modelsForFetchFallback(
|
|
3825
|
-
|
|
3826
|
-
if (availableModelsProvider === provider && cached.length) return cached;
|
|
3827
|
-
return defaultModelsForProvider(provider);
|
|
3593
|
+
function modelsForFetchFallback() {
|
|
3594
|
+
return getAvailableModels().length ? getAvailableModels() : defaultModels();
|
|
3828
3595
|
}
|
|
3829
3596
|
|
|
3830
3597
|
async function fetchModels(opts = {}) {
|
|
3831
3598
|
const refresh = opts.refresh === true;
|
|
3832
|
-
const
|
|
3833
|
-
const reconcileProvider = opts.reconcileProvider === true;
|
|
3599
|
+
const reconcile = opts.reconcile === true;
|
|
3834
3600
|
const reconcileCandidate = opts.reconcileCandidate;
|
|
3835
|
-
if (
|
|
3601
|
+
if (reconcile) {
|
|
3836
3602
|
clearAvailableModels();
|
|
3837
3603
|
}
|
|
3838
|
-
const query = new URLSearchParams(
|
|
3604
|
+
const query = new URLSearchParams();
|
|
3839
3605
|
if (refresh) query.set('refresh', '1');
|
|
3840
3606
|
try {
|
|
3841
3607
|
const res = await fetch(`/api/models?${query.toString()}`);
|
|
3842
3608
|
if (!res.ok) {
|
|
3843
3609
|
populateSettingsModels({
|
|
3844
|
-
models: modelsForFetchFallback(
|
|
3610
|
+
models: modelsForFetchFallback(),
|
|
3845
3611
|
selected: currentModel,
|
|
3846
|
-
|
|
3847
|
-
reconcileProvider,
|
|
3612
|
+
reconcile,
|
|
3848
3613
|
reconcileCandidate,
|
|
3849
3614
|
});
|
|
3850
3615
|
return;
|
|
@@ -3852,17 +3617,15 @@ async function fetchModels(opts = {}) {
|
|
|
3852
3617
|
const data = await readJson(res);
|
|
3853
3618
|
populateSettingsModels({
|
|
3854
3619
|
...data,
|
|
3855
|
-
|
|
3856
|
-
reconcileProvider,
|
|
3620
|
+
reconcile,
|
|
3857
3621
|
reconcileCandidate,
|
|
3858
3622
|
});
|
|
3859
3623
|
} catch (err) {
|
|
3860
3624
|
console.error('Failed to fetch models:', err);
|
|
3861
3625
|
populateSettingsModels({
|
|
3862
|
-
models: modelsForFetchFallback(
|
|
3626
|
+
models: modelsForFetchFallback(),
|
|
3863
3627
|
selected: currentModel,
|
|
3864
|
-
|
|
3865
|
-
reconcileProvider,
|
|
3628
|
+
reconcile,
|
|
3866
3629
|
reconcileCandidate,
|
|
3867
3630
|
});
|
|
3868
3631
|
}
|
|
@@ -3982,13 +3745,6 @@ function claudeStatusText(cfg) {
|
|
|
3982
3745
|
}
|
|
3983
3746
|
}
|
|
3984
3747
|
|
|
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
3748
|
function fillAuthSettings(cfg) {
|
|
3993
3749
|
if (els.settingsGhStatus) {
|
|
3994
3750
|
els.settingsGhStatus.textContent = ghStatusText(cfg);
|
|
@@ -4047,25 +3803,6 @@ function fillAuthSettings(cfg) {
|
|
|
4047
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.'
|
|
4048
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.';
|
|
4049
3805
|
}
|
|
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
3806
|
}
|
|
4070
3807
|
|
|
4071
3808
|
function fillSettingsForm(cfg) {
|
|
@@ -4145,23 +3882,17 @@ function fillSettingsForm(cfg) {
|
|
|
4145
3882
|
els.settingsBaseBranch.value = cfg.baseBranch || '';
|
|
4146
3883
|
}
|
|
4147
3884
|
|
|
4148
|
-
if (els.settingsLlmProvider) {
|
|
4149
|
-
syncLlmProviderSelects(cfg.llmProvider === 'openrouter' ? 'openrouter' : 'claude');
|
|
4150
|
-
}
|
|
4151
|
-
|
|
4152
3885
|
if (els.settingsModelPicker) {
|
|
4153
|
-
|
|
4154
|
-
if (getAvailableModels(provider).length) {
|
|
3886
|
+
if (getAvailableModels().length) {
|
|
4155
3887
|
populateSettingsModels({
|
|
4156
|
-
models: getAvailableModels(
|
|
3888
|
+
models: getAvailableModels(),
|
|
4157
3889
|
selected: cfg.model ?? currentModel,
|
|
4158
|
-
provider,
|
|
4159
3890
|
});
|
|
4160
3891
|
} else if (cfg.model != null) {
|
|
4161
3892
|
currentModel = isNoModelSelection(cfg.model) ? NO_MODEL : cfg.model;
|
|
4162
3893
|
syncModelPickerValue(currentModel);
|
|
4163
3894
|
}
|
|
4164
|
-
void fetchModels(
|
|
3895
|
+
void fetchModels();
|
|
4165
3896
|
}
|
|
4166
3897
|
|
|
4167
3898
|
if (els.settingsMaxTurns) {
|
|
@@ -4354,16 +4085,14 @@ function setOverviewLlmFeedback(message, kind = 'ok') {
|
|
|
4354
4085
|
*/
|
|
4355
4086
|
function fillOverviewLlmControls(cfg) {
|
|
4356
4087
|
if (!cfg) return;
|
|
4357
|
-
const provider = cfg.llmProvider === 'openrouter' ? 'openrouter' : 'claude';
|
|
4358
|
-
syncLlmProviderSelects(provider);
|
|
4359
4088
|
syncAllModelComboboxValues(currentModel);
|
|
4360
4089
|
}
|
|
4361
4090
|
|
|
4362
4091
|
let llmConfigSaving = false;
|
|
4363
4092
|
|
|
4364
4093
|
/**
|
|
4365
|
-
* PATCH
|
|
4366
|
-
* @param {{
|
|
4094
|
+
* PATCH model immediately (Overview switcher).
|
|
4095
|
+
* @param {{ model?: string }} patch
|
|
4367
4096
|
*/
|
|
4368
4097
|
async function patchLlmConfig(patch) {
|
|
4369
4098
|
if (llmConfigSaving) return;
|
|
@@ -4385,7 +4114,7 @@ async function patchLlmConfig(patch) {
|
|
|
4385
4114
|
if (currentView === 'settings') {
|
|
4386
4115
|
fillSettingsForm(data);
|
|
4387
4116
|
}
|
|
4388
|
-
setOverviewLlmFeedback('
|
|
4117
|
+
setOverviewLlmFeedback('Model updated. Next jobs will use this value.', 'ok');
|
|
4389
4118
|
window.setTimeout(() => setOverviewLlmFeedback(null), 2500);
|
|
4390
4119
|
} catch (err) {
|
|
4391
4120
|
setOverviewLlmFeedback(err.message || 'Update failed', 'error');
|
|
@@ -4394,29 +4123,6 @@ async function patchLlmConfig(patch) {
|
|
|
4394
4123
|
}
|
|
4395
4124
|
}
|
|
4396
4125
|
|
|
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
4126
|
function applyConfigSnapshot(data) {
|
|
4421
4127
|
appConfig = data || {};
|
|
4422
4128
|
if (data?.model != null) {
|
|
@@ -4432,7 +4138,6 @@ function applyConfigSnapshot(data) {
|
|
|
4432
4138
|
if (els.repoBranch) {
|
|
4433
4139
|
els.repoBranch.textContent = data?.baseBranch || '—';
|
|
4434
4140
|
}
|
|
4435
|
-
syncLlmProviderAvailability(appConfig);
|
|
4436
4141
|
if (currentView === 'settings') {
|
|
4437
4142
|
fillSettingsForm(appConfig);
|
|
4438
4143
|
}
|
|
@@ -4468,7 +4173,6 @@ function readSettingsForm() {
|
|
|
4468
4173
|
const patch = {
|
|
4469
4174
|
baseBranch,
|
|
4470
4175
|
model,
|
|
4471
|
-
llmProvider: els.settingsLlmProvider?.value === 'openrouter' ? 'openrouter' : 'claude',
|
|
4472
4176
|
maxAgentTurns,
|
|
4473
4177
|
agentTimeoutMs: Math.round(timeoutMinutes * 60_000),
|
|
4474
4178
|
testCommand: testTrimmed === '' ? null : testTrimmed,
|
|
@@ -4482,8 +4186,6 @@ function readSettingsForm() {
|
|
|
4482
4186
|
if (anthropicApiKey !== undefined) patch.anthropicApiKey = anthropicApiKey;
|
|
4483
4187
|
const claudeOauthToken = secretPatchValue(els.settingsClaudeOauth);
|
|
4484
4188
|
if (claudeOauthToken !== undefined) patch.claudeOauthToken = claudeOauthToken;
|
|
4485
|
-
const openrouterApiKey = secretPatchValue(els.settingsOpenrouterKey);
|
|
4486
|
-
if (openrouterApiKey !== undefined) patch.openrouterApiKey = openrouterApiKey;
|
|
4487
4189
|
|
|
4488
4190
|
if (ticketSource === 'jira' || els.settingsJiraBaseUrl?.value) {
|
|
4489
4191
|
patch.jiraBaseUrl = els.settingsJiraBaseUrl?.value?.trim() || '';
|
|
@@ -4651,12 +4353,6 @@ bindSecretClear(
|
|
|
4651
4353
|
els.settingsClaudeOauthHint,
|
|
4652
4354
|
'Saved CLAUDE_CODE_OAUTH_TOKEN will be removed when you click Save settings.'
|
|
4653
4355
|
);
|
|
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
4356
|
|
|
4661
4357
|
|
|
4662
4358
|
document.addEventListener('click', (ev) => {
|
|
@@ -4689,38 +4385,6 @@ document.addEventListener('keydown', (ev) => {
|
|
|
4689
4385
|
}
|
|
4690
4386
|
});
|
|
4691
4387
|
|
|
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
4388
|
if (
|
|
4725
4389
|
els.settingsModelPicker &&
|
|
4726
4390
|
els.settingsModelTrigger &&
|
|
@@ -4763,9 +4427,6 @@ if (
|
|
|
4763
4427
|
list: els.overviewAgentModelList,
|
|
4764
4428
|
valueEl: els.overviewAgentValue,
|
|
4765
4429
|
iconEl: els.overviewAgentIcon,
|
|
4766
|
-
tabs: [
|
|
4767
|
-
...(els.overviewAgentPanel.querySelectorAll('.agent-picker-tab') || []),
|
|
4768
|
-
],
|
|
4769
4430
|
hiddenInput: els.overviewModel,
|
|
4770
4431
|
optionIdPrefix: 'overview-agent-model-opt',
|
|
4771
4432
|
onSelect: (modelId) => {
|
|
@@ -5096,9 +4757,8 @@ els.logJobFilter.addEventListener('change', () => {
|
|
|
5096
4757
|
|
|
5097
4758
|
applyTheme(loadTheme());
|
|
5098
4759
|
void (async () => {
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
await fetchModels({ provider });
|
|
4760
|
+
await fetchConfig();
|
|
4761
|
+
await fetchModels();
|
|
5102
4762
|
})();
|
|
5103
4763
|
fetchJobs();
|
|
5104
4764
|
setInterval(fetchJobs, 3000);
|