acdev 1.0.12 → 1.0.13
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/package.json +1 -1
- package/public/app.js +87 -7
- package/public/index.html +1 -1
- package/public/styles.css +11 -1
- package/src/config.js +36 -0
- package/src/server.js +21 -1
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -344,6 +344,7 @@ const els = {
|
|
|
344
344
|
settingsOpenrouterKeyClear: document.getElementById('settings-openrouter-key-clear'),
|
|
345
345
|
settingsOpenrouterKeyHint: document.getElementById('settings-openrouter-key-hint'),
|
|
346
346
|
settingsLlmProvider: document.getElementById('settings-llm-provider'),
|
|
347
|
+
settingsLlmProviderHint: document.getElementById('settings-llm-provider-hint'),
|
|
347
348
|
overviewLlmProvider: document.getElementById('overview-llm-provider'),
|
|
348
349
|
sidebarAgentLabel: document.getElementById('sidebar-agent-label'),
|
|
349
350
|
settingsTabs: document.getElementById('settings-tabs'),
|
|
@@ -3973,7 +3974,7 @@ function fillSettingsForm(cfg) {
|
|
|
3973
3974
|
fillAuthSettings(cfg);
|
|
3974
3975
|
|
|
3975
3976
|
updateTicketSourceUI(cfg.ticketSource === 'jira' ? 'jira' : 'github');
|
|
3976
|
-
updateLlmProviderUI(cfg.llmProvider === 'openrouter' ? 'openrouter' : 'claude');
|
|
3977
|
+
updateLlmProviderUI(cfg.llmProvider === 'openrouter' ? 'openrouter' : 'claude', cfg);
|
|
3977
3978
|
|
|
3978
3979
|
if (els.settingsJiraBaseUrl) {
|
|
3979
3980
|
els.settingsJiraBaseUrl.value = cfg.jiraBaseUrl || '';
|
|
@@ -4202,18 +4203,74 @@ function updateTicketSourceUI(source) {
|
|
|
4202
4203
|
|
|
4203
4204
|
/**
|
|
4204
4205
|
* @param {'claude' | 'openrouter'} provider
|
|
4206
|
+
* @param {typeof appConfig} [cfg]
|
|
4205
4207
|
*/
|
|
4206
|
-
function
|
|
4208
|
+
function llmProviderReady(provider, cfg = appConfig) {
|
|
4209
|
+
if (cfg?.stubAgent) return true;
|
|
4210
|
+
if (provider === 'openrouter') return cfg?.openrouterAuthOk === true;
|
|
4211
|
+
return cfg?.claudeAuthOk === true;
|
|
4212
|
+
}
|
|
4213
|
+
|
|
4214
|
+
/**
|
|
4215
|
+
* @param {'claude' | 'openrouter'} provider
|
|
4216
|
+
*/
|
|
4217
|
+
function llmProviderUnavailableReason(provider) {
|
|
4218
|
+
if (provider === 'openrouter') {
|
|
4219
|
+
return 'OpenRouter is not configured. Add an API key in Settings → Authentication first.';
|
|
4220
|
+
}
|
|
4221
|
+
return 'Claude is not configured. Add an API key or OAuth token in Settings → Authentication, or run claude auth login.';
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4224
|
+
const LLM_PROVIDER_HINT_DEFAULT =
|
|
4225
|
+
'Claude uses the Claude Agent SDK. OpenRouter uses <code>@openrouter/agent</code> with the same coding tools (any catalog model). A provider stays disabled until it is configured and authenticated.';
|
|
4226
|
+
|
|
4227
|
+
/**
|
|
4228
|
+
* @param {typeof appConfig} [cfg]
|
|
4229
|
+
*/
|
|
4230
|
+
function updateLlmProviderHint(cfg = appConfig) {
|
|
4231
|
+
if (!els.settingsLlmProviderHint) return;
|
|
4232
|
+
if (cfg?.stubAgent) {
|
|
4233
|
+
els.settingsLlmProviderHint.innerHTML = LLM_PROVIDER_HINT_DEFAULT;
|
|
4234
|
+
return;
|
|
4235
|
+
}
|
|
4236
|
+
const missing = [];
|
|
4237
|
+
if (!llmProviderReady('claude', cfg)) {
|
|
4238
|
+
missing.push('Claude (API key, OAuth token, or <code>claude auth login</code>)');
|
|
4239
|
+
}
|
|
4240
|
+
if (!llmProviderReady('openrouter', cfg)) {
|
|
4241
|
+
missing.push('OpenRouter (API key in Authentication)');
|
|
4242
|
+
}
|
|
4243
|
+
if (!missing.length) {
|
|
4244
|
+
els.settingsLlmProviderHint.innerHTML = LLM_PROVIDER_HINT_DEFAULT;
|
|
4245
|
+
return;
|
|
4246
|
+
}
|
|
4247
|
+
els.settingsLlmProviderHint.innerHTML = `Cannot enable a provider until it is configured and authenticated. Missing: ${missing.join('; ')}.`;
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4250
|
+
/**
|
|
4251
|
+
* @param {'claude' | 'openrouter'} provider
|
|
4252
|
+
* @param {typeof appConfig} [cfg]
|
|
4253
|
+
*/
|
|
4254
|
+
function updateLlmProviderUI(provider, cfg = appConfig) {
|
|
4207
4255
|
llmProvider = provider === 'openrouter' ? 'openrouter' : 'claude';
|
|
4208
4256
|
|
|
4209
4257
|
for (const toggle of [els.settingsLlmProvider, els.overviewLlmProvider]) {
|
|
4210
4258
|
if (!toggle) continue;
|
|
4211
4259
|
toggle.querySelectorAll('.source-btn').forEach((btn) => {
|
|
4212
|
-
const
|
|
4260
|
+
const id = btn.dataset.provider === 'openrouter' ? 'openrouter' : 'claude';
|
|
4261
|
+
const active = id === llmProvider;
|
|
4262
|
+
const ready = llmProviderReady(id, cfg);
|
|
4213
4263
|
btn.classList.toggle('active', active);
|
|
4214
4264
|
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
4265
|
+
btn.disabled = !ready;
|
|
4266
|
+
if (!ready) {
|
|
4267
|
+
btn.title = llmProviderUnavailableReason(id);
|
|
4268
|
+
} else {
|
|
4269
|
+
btn.removeAttribute('title');
|
|
4270
|
+
}
|
|
4215
4271
|
});
|
|
4216
4272
|
}
|
|
4273
|
+
updateLlmProviderHint(cfg);
|
|
4217
4274
|
updateModelLabel();
|
|
4218
4275
|
}
|
|
4219
4276
|
|
|
@@ -4240,6 +4297,13 @@ async function saveTicketSource(next) {
|
|
|
4240
4297
|
|
|
4241
4298
|
async function saveLlmProvider(next) {
|
|
4242
4299
|
const provider = next === 'openrouter' ? 'openrouter' : 'claude';
|
|
4300
|
+
if (!llmProviderReady(provider)) {
|
|
4301
|
+
const msg = llmProviderUnavailableReason(provider);
|
|
4302
|
+
setOverviewLlmFeedback(msg, 'error');
|
|
4303
|
+
setSettingsFeedback(msg, 'error');
|
|
4304
|
+
return;
|
|
4305
|
+
}
|
|
4306
|
+
const prev = llmProvider;
|
|
4243
4307
|
updateLlmProviderUI(provider);
|
|
4244
4308
|
clearAvailableModels();
|
|
4245
4309
|
try {
|
|
@@ -4249,11 +4313,18 @@ async function saveLlmProvider(next) {
|
|
|
4249
4313
|
body: JSON.stringify({ llmProvider: provider }),
|
|
4250
4314
|
});
|
|
4251
4315
|
const data = await readJson(res);
|
|
4252
|
-
if (res.ok) {
|
|
4253
|
-
|
|
4316
|
+
if (!res.ok) {
|
|
4317
|
+
updateLlmProviderUI(prev);
|
|
4318
|
+
const msg = data.error || `Update failed (HTTP ${res.status})`;
|
|
4319
|
+
setOverviewLlmFeedback(msg, 'error');
|
|
4320
|
+
setSettingsFeedback(msg, 'error');
|
|
4254
4321
|
await fetchModels({ refresh: true, reconcile: true });
|
|
4322
|
+
return;
|
|
4255
4323
|
}
|
|
4324
|
+
applyConfigSnapshot({ ...appConfig, ...data });
|
|
4325
|
+
await fetchModels({ refresh: true, reconcile: true });
|
|
4256
4326
|
} catch {
|
|
4327
|
+
updateLlmProviderUI(prev);
|
|
4257
4328
|
void fetchModels({ refresh: true, reconcile: true });
|
|
4258
4329
|
}
|
|
4259
4330
|
}
|
|
@@ -4285,7 +4356,7 @@ function setOverviewLlmFeedback(message, kind = 'ok') {
|
|
|
4285
4356
|
*/
|
|
4286
4357
|
function fillOverviewLlmControls(cfg) {
|
|
4287
4358
|
if (!cfg) return;
|
|
4288
|
-
updateLlmProviderUI(cfg.llmProvider === 'openrouter' ? 'openrouter' : 'claude');
|
|
4359
|
+
updateLlmProviderUI(cfg.llmProvider === 'openrouter' ? 'openrouter' : 'claude', cfg);
|
|
4289
4360
|
syncAllModelComboboxValues(currentModel);
|
|
4290
4361
|
}
|
|
4291
4362
|
|
|
@@ -4333,7 +4404,10 @@ function applyConfigSnapshot(data) {
|
|
|
4333
4404
|
updateModelLabel();
|
|
4334
4405
|
fillOverviewLlmControls(data);
|
|
4335
4406
|
updateTicketSourceUI(data?.ticketSource === 'jira' ? 'jira' : 'github');
|
|
4336
|
-
updateLlmProviderUI(
|
|
4407
|
+
updateLlmProviderUI(
|
|
4408
|
+
data?.llmProvider === 'openrouter' ? 'openrouter' : 'claude',
|
|
4409
|
+
data || appConfig
|
|
4410
|
+
);
|
|
4337
4411
|
if (els.repoName) {
|
|
4338
4412
|
els.repoName.textContent = data?.repoName || 'local repo';
|
|
4339
4413
|
}
|
|
@@ -4730,6 +4804,12 @@ function handleLlmProviderToggleClick(e) {
|
|
|
4730
4804
|
const next = btn.dataset.provider === 'openrouter' ? 'openrouter' : 'claude';
|
|
4731
4805
|
if (next === llmProvider) return;
|
|
4732
4806
|
e.stopPropagation();
|
|
4807
|
+
if (btn.disabled || !llmProviderReady(next)) {
|
|
4808
|
+
const msg = llmProviderUnavailableReason(next);
|
|
4809
|
+
setOverviewLlmFeedback(msg, 'error');
|
|
4810
|
+
setSettingsFeedback(msg, 'error');
|
|
4811
|
+
return;
|
|
4812
|
+
}
|
|
4733
4813
|
void saveLlmProvider(next);
|
|
4734
4814
|
}
|
|
4735
4815
|
|
package/public/index.html
CHANGED
|
@@ -669,7 +669,7 @@
|
|
|
669
669
|
<button type="button" class="source-btn active" data-provider="claude">Claude</button>
|
|
670
670
|
<button type="button" class="source-btn" data-provider="openrouter">OpenRouter</button>
|
|
671
671
|
</div>
|
|
672
|
-
<p class="field-hint">Claude uses the Claude Agent SDK. OpenRouter uses <code>@openrouter/agent</code> with the same coding tools (any catalog model).</p>
|
|
672
|
+
<p class="field-hint" id="settings-llm-provider-hint">Claude uses the Claude Agent SDK. OpenRouter uses <code>@openrouter/agent</code> with the same coding tools (any catalog model). A provider stays disabled until it is configured and authenticated.</p>
|
|
673
673
|
</div>
|
|
674
674
|
<div class="settings-grid">
|
|
675
675
|
<div class="settings-field">
|
package/public/styles.css
CHANGED
|
@@ -2475,7 +2475,7 @@ body.diff-fs-open {
|
|
|
2475
2475
|
letter-spacing: 0.02em;
|
|
2476
2476
|
}
|
|
2477
2477
|
|
|
2478
|
-
.source-btn:hover:not(.active) {
|
|
2478
|
+
.source-btn:hover:not(.active):not(:disabled) {
|
|
2479
2479
|
color: var(--text);
|
|
2480
2480
|
background: color-mix(in srgb, var(--text) 4%, transparent);
|
|
2481
2481
|
}
|
|
@@ -2487,6 +2487,16 @@ body.diff-fs-open {
|
|
|
2487
2487
|
box-shadow: var(--shadow-sm);
|
|
2488
2488
|
}
|
|
2489
2489
|
|
|
2490
|
+
.source-btn:disabled {
|
|
2491
|
+
opacity: 0.45;
|
|
2492
|
+
cursor: not-allowed;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
.source-btn.active:disabled {
|
|
2496
|
+
opacity: 0.7;
|
|
2497
|
+
cursor: default;
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2490
2500
|
.enqueue-jira-hint {
|
|
2491
2501
|
margin: 0;
|
|
2492
2502
|
font-size: 12px;
|
package/src/config.js
CHANGED
|
@@ -91,6 +91,42 @@ export function normalizeLlmProvider(value) {
|
|
|
91
91
|
return value === 'openrouter' ? 'openrouter' : 'claude';
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Block enabling an LLM provider that is not authenticated.
|
|
96
|
+
* Stub-agent skips the check (UI-only testing without credentials).
|
|
97
|
+
* @param {unknown} nextProvider
|
|
98
|
+
* @param {{
|
|
99
|
+
* stubAgent?: boolean,
|
|
100
|
+
* claudeAuth?: import('./claude-auth.js').ClaudeAuthResult,
|
|
101
|
+
* openrouterAuth?: import('./openrouter-auth.js').OpenRouterAuthResult,
|
|
102
|
+
* }} [opts]
|
|
103
|
+
* @returns {{ error: string, code: string } | null}
|
|
104
|
+
*/
|
|
105
|
+
export function llmProviderAuthGate(nextProvider, opts = {}) {
|
|
106
|
+
if (opts.stubAgent === true) return null;
|
|
107
|
+
const provider = normalizeLlmProvider(nextProvider);
|
|
108
|
+
if (provider === 'openrouter') {
|
|
109
|
+
const or = opts.openrouterAuth ?? checkOpenRouterAuth();
|
|
110
|
+
if (!or.ok) {
|
|
111
|
+
return {
|
|
112
|
+
error:
|
|
113
|
+
'OpenRouter is not authenticated. Add an API key in Settings → Authentication before enabling OpenRouter.',
|
|
114
|
+
code: 'openrouter_auth_required',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const claude = opts.claudeAuth ?? checkClaudeAuth();
|
|
120
|
+
if (!claude.ok) {
|
|
121
|
+
return {
|
|
122
|
+
error:
|
|
123
|
+
'Claude is not authenticated. Add an API key or OAuth token in Settings → Authentication, or run claude auth login, before enabling Claude.',
|
|
124
|
+
code: 'claude_auth_required',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
94
130
|
/**
|
|
95
131
|
* @param {unknown} raw
|
|
96
132
|
* @returns {{ claude?: string, openrouter?: string }}
|
package/src/server.js
CHANGED
|
@@ -33,7 +33,12 @@ import {
|
|
|
33
33
|
runAgentOnReviewFeedback,
|
|
34
34
|
stripAiAttribution,
|
|
35
35
|
} from './agent.js';
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
publicConfig,
|
|
38
|
+
updateConfig,
|
|
39
|
+
normalizeLlmProvider,
|
|
40
|
+
llmProviderAuthGate,
|
|
41
|
+
} from './config.js';
|
|
37
42
|
import { upsertEnvVars } from './env.js';
|
|
38
43
|
import { listModels } from './models.js';
|
|
39
44
|
import { splitIssueUrls } from './urls.js';
|
|
@@ -682,6 +687,21 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
682
687
|
openrouterApiKey: _or,
|
|
683
688
|
...configPatch
|
|
684
689
|
} = patch;
|
|
690
|
+
|
|
691
|
+
if (configPatch.llmProvider === 'claude' || configPatch.llmProvider === 'openrouter') {
|
|
692
|
+
const next = configPatch.llmProvider;
|
|
693
|
+
if (next !== normalizeLlmProvider(config.llmProvider)) {
|
|
694
|
+
const gate = llmProviderAuthGate(next, {
|
|
695
|
+
stubAgent: useStubAgent,
|
|
696
|
+
claudeAuth: doCheckClaudeAuth(),
|
|
697
|
+
openrouterAuth: checkOpenRouterAuth(),
|
|
698
|
+
});
|
|
699
|
+
if (gate) {
|
|
700
|
+
return res.status(400).json(gate);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
685
705
|
updateConfig(repoRoot, config, configPatch);
|
|
686
706
|
res.json(publicConfigPayload());
|
|
687
707
|
} catch (err) {
|