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/.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 +80 -353
- package/public/index.html +1 -33
- package/public/styles.css +7 -4
- package/src/agent.js +64 -127
- package/src/config.js +45 -151
- package/src/models.js +34 -225
- package/src/server.js +11 -50
- package/src/openrouter-auth.js +0 -37
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(
|
|
2988
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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' | '
|
|
3657
|
-
* @param {'claude' | 'openrouter'} [provider]
|
|
3527
|
+
* @param {'anthropic' | 'fallback' | string | undefined} source
|
|
3658
3528
|
*/
|
|
3659
|
-
function updateModelSourceHint(source
|
|
3529
|
+
function updateModelSourceHint(source) {
|
|
3660
3530
|
if (!els.settingsModelHint) return;
|
|
3661
|
-
const name = provider === 'openrouter' ? 'OpenRouter' : 'Claude';
|
|
3662
3531
|
if (source === 'anthropic') {
|
|
3663
|
-
|
|
3664
|
-
|
|
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
|
-
|
|
3670
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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?.
|
|
3567
|
+
: data?.reconcile
|
|
3738
3568
|
? []
|
|
3739
|
-
:
|
|
3740
|
-
?
|
|
3741
|
-
:
|
|
3742
|
-
|
|
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?.
|
|
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
|
|
3584
|
+
currentModel = reconcileModelForProvider(models, next);
|
|
3753
3585
|
}
|
|
3754
|
-
if (data?.source) updateModelSourceHint(data.source
|
|
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(
|
|
3762
|
-
|
|
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
|
|
3771
|
-
const reconcileProvider = opts.reconcileProvider === true;
|
|
3599
|
+
const reconcile = opts.reconcile === true;
|
|
3772
3600
|
const reconcileCandidate = opts.reconcileCandidate;
|
|
3773
|
-
if (
|
|
3601
|
+
if (reconcile) {
|
|
3774
3602
|
clearAvailableModels();
|
|
3775
3603
|
}
|
|
3776
|
-
const query = new URLSearchParams(
|
|
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(
|
|
3610
|
+
models: modelsForFetchFallback(),
|
|
3783
3611
|
selected: currentModel,
|
|
3784
|
-
|
|
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
|
-
|
|
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(
|
|
3626
|
+
models: modelsForFetchFallback(),
|
|
3801
3627
|
selected: currentModel,
|
|
3802
|
-
|
|
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
|
-
|
|
4092
|
-
if (getAvailableModels(provider).length) {
|
|
3886
|
+
if (getAvailableModels().length) {
|
|
4093
3887
|
populateSettingsModels({
|
|
4094
|
-
models: getAvailableModels(
|
|
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(
|
|
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
|
|
4304
|
-
* @param {{
|
|
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('
|
|
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
|
-
|
|
5035
|
-
|
|
4759
|
+
void (async () => {
|
|
4760
|
+
await fetchConfig();
|
|
4761
|
+
await fetchModels();
|
|
4762
|
+
})();
|
|
5036
4763
|
fetchJobs();
|
|
5037
4764
|
setInterval(fetchJobs, 3000);
|