acdev 1.0.9 → 1.0.11
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 +3 -0
- package/bin/acdev.js +12 -4
- package/package.json +4 -2
- package/public/app.js +161 -6
- package/public/index.html +47 -2
- package/public/styles.css +47 -1
- package/src/agent.js +68 -21
- package/src/config.js +122 -10
- package/src/git.js +62 -0
- package/src/models.js +131 -18
- package/src/openrouter-agent.js +142 -0
- package/src/openrouter-auth.js +41 -0
- package/src/openrouter-tools.js +291 -0
- package/src/server.js +60 -9
- package/src/store.js +2 -0
- package/src/usage.js +35 -0
package/.acdev/.env.example
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
# An API key takes precedence over subscription login:
|
|
14
14
|
# ANTHROPIC_API_KEY=sk-ant-your-key-here
|
|
15
15
|
#
|
|
16
|
+
# OpenRouter (Settings → switch LLM provider to OpenRouter):
|
|
17
|
+
# OPENROUTER_API_KEY=sk-or-your-key-here
|
|
18
|
+
#
|
|
16
19
|
# Jira Cloud (only when ticketSource is "jira" in Settings / config.json):
|
|
17
20
|
# JIRA_BASE_URL=https://your-domain.atlassian.net
|
|
18
21
|
# JIRA_EMAIL=you@company.com
|
package/bin/acdev.js
CHANGED
|
@@ -4,8 +4,9 @@ import { execSync } from 'node:child_process';
|
|
|
4
4
|
import http from 'node:http';
|
|
5
5
|
import open from 'open';
|
|
6
6
|
import { checkClaudeAuth, formatClaudeAuthError } from '../src/claude-auth.js';
|
|
7
|
+
import { checkOpenRouterAuth, formatOpenRouterAuthError } from '../src/openrouter-auth.js';
|
|
7
8
|
import { checkGhAuth, formatGhAuthError } from '../src/gh-auth.js';
|
|
8
|
-
import { loadConfig } from '../src/config.js';
|
|
9
|
+
import { loadConfig, normalizeLlmProvider } from '../src/config.js';
|
|
9
10
|
import { loadEnv } from '../src/env.js';
|
|
10
11
|
import { migrateLegacyDataDir, migrateLegacyWorktreesDir } from '../src/paths.js';
|
|
11
12
|
import { Store } from '../src/store.js';
|
|
@@ -75,9 +76,16 @@ async function main() {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
if (!opts.stubAgent) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
if (normalizeLlmProvider(config.llmProvider) === 'openrouter') {
|
|
80
|
+
const openrouterAuth = checkOpenRouterAuth();
|
|
81
|
+
if (!openrouterAuth.ok) {
|
|
82
|
+
console.warn(formatOpenRouterAuthError(openrouterAuth));
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
const claudeAuth = checkClaudeAuth();
|
|
86
|
+
if (!claudeAuth.ok) {
|
|
87
|
+
console.warn(formatClaudeAuthError(claudeAuth));
|
|
88
|
+
}
|
|
81
89
|
}
|
|
82
90
|
} else {
|
|
83
91
|
console.log('✓ Stub agent enabled (LLM auth not required)');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "acdev",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "Local CLI + web UI for running AI agents on GitHub issues via git worktrees",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,11 +22,13 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@anthropic-ai/claude-agent-sdk": "0.1.77",
|
|
25
|
+
"@openrouter/agent": "^0.11.0",
|
|
25
26
|
"dotenv": "^17.4.2",
|
|
26
27
|
"express": "^4.21.2",
|
|
27
28
|
"open": "^10.1.0",
|
|
28
29
|
"simple-git": "^3.27.0",
|
|
29
|
-
"uuid": "^11.1.0"
|
|
30
|
+
"uuid": "^11.1.0",
|
|
31
|
+
"zod": "^4.5.4"
|
|
30
32
|
},
|
|
31
33
|
"publishConfig": {
|
|
32
34
|
"access": "public"
|
package/public/app.js
CHANGED
|
@@ -160,6 +160,8 @@ let currentModel = 'claude-sonnet-5';
|
|
|
160
160
|
let availableModels = [];
|
|
161
161
|
/** @type {'github' | 'jira'} */
|
|
162
162
|
let ticketSource = 'github';
|
|
163
|
+
/** @type {'claude' | 'openrouter'} */
|
|
164
|
+
let llmProvider = 'claude';
|
|
163
165
|
/** @type {{
|
|
164
166
|
* repoName?: string,
|
|
165
167
|
* baseBranch?: string,
|
|
@@ -170,6 +172,8 @@ let ticketSource = 'github';
|
|
|
170
172
|
* knownTools?: string[],
|
|
171
173
|
* models?: Array<{id:string,label:string}>,
|
|
172
174
|
* model?: string,
|
|
175
|
+
* llmProvider?: 'claude' | 'openrouter',
|
|
176
|
+
* lastModelsByProvider?: { claude?: string, openrouter?: string },
|
|
173
177
|
* ticketSource?: 'github' | 'jira',
|
|
174
178
|
* jiraBaseUrl?: string,
|
|
175
179
|
* jiraEmail?: string | null,
|
|
@@ -190,6 +194,9 @@ let ticketSource = 'github';
|
|
|
190
194
|
* anthropicApiKeyMasked?: string | null,
|
|
191
195
|
* claudeOauthTokenSet?: boolean,
|
|
192
196
|
* claudeOauthTokenMasked?: string | null,
|
|
197
|
+
* openrouterApiKeySet?: boolean,
|
|
198
|
+
* openrouterApiKeyMasked?: string | null,
|
|
199
|
+
* openrouterAuthOk?: boolean,
|
|
193
200
|
* llmAuthOk?: boolean,
|
|
194
201
|
* stubAgent?: boolean,
|
|
195
202
|
* }} */
|
|
@@ -228,6 +235,7 @@ const els = {
|
|
|
228
235
|
enqueueJiraSettingsLink: document.getElementById('enqueue-jira-settings-link'),
|
|
229
236
|
overviewTicketSource: document.getElementById('overview-ticket-source'),
|
|
230
237
|
enqueueFeedback: document.getElementById('enqueue-feedback'),
|
|
238
|
+
preferredBranch: document.getElementById('preferred-branch'),
|
|
231
239
|
addBtn: document.getElementById('add-btn'),
|
|
232
240
|
statsGrid: document.getElementById('stats-grid'),
|
|
233
241
|
overviewRunning: document.getElementById('overview-running'),
|
|
@@ -330,6 +338,13 @@ const els = {
|
|
|
330
338
|
settingsClaudeOauth: document.getElementById('settings-claude-oauth'),
|
|
331
339
|
settingsClaudeOauthClear: document.getElementById('settings-claude-oauth-clear'),
|
|
332
340
|
settingsClaudeOauthHint: document.getElementById('settings-claude-oauth-hint'),
|
|
341
|
+
settingsOpenrouterStatus: document.getElementById('settings-openrouter-status'),
|
|
342
|
+
settingsOpenrouterKey: document.getElementById('settings-openrouter-key'),
|
|
343
|
+
settingsOpenrouterKeyClear: document.getElementById('settings-openrouter-key-clear'),
|
|
344
|
+
settingsOpenrouterKeyHint: document.getElementById('settings-openrouter-key-hint'),
|
|
345
|
+
settingsLlmProvider: document.getElementById('settings-llm-provider'),
|
|
346
|
+
overviewLlmProvider: document.getElementById('overview-llm-provider'),
|
|
347
|
+
sidebarAgentLabel: document.getElementById('sidebar-agent-label'),
|
|
333
348
|
settingsTabs: document.getElementById('settings-tabs'),
|
|
334
349
|
};
|
|
335
350
|
|
|
@@ -900,16 +915,22 @@ function jobTitle(job) {
|
|
|
900
915
|
return job.issueTitle || truncate(job.issueUrl, 48);
|
|
901
916
|
}
|
|
902
917
|
|
|
918
|
+
function jobDisplayBranch(job) {
|
|
919
|
+
return job.branchName || job.preferredBranchName || '';
|
|
920
|
+
}
|
|
921
|
+
|
|
903
922
|
function jobRepoLine(job) {
|
|
904
923
|
if (isJiraJob(job)) {
|
|
905
924
|
const key = parseJiraKeyFromJob(job) || '—';
|
|
906
|
-
const
|
|
925
|
+
const name = jobDisplayBranch(job);
|
|
926
|
+
const branch = name ? ` · ${name}` : '';
|
|
907
927
|
return { repo: 'Jira', number: key, branch, text: `Jira ${key}${branch}`, isJira: true };
|
|
908
928
|
}
|
|
909
929
|
const ref = parseIssueRef(job.issueUrl);
|
|
910
930
|
const repo = ref?.full || '—';
|
|
911
931
|
const num = ref?.number || job.issueNumber || '—';
|
|
912
|
-
const
|
|
932
|
+
const name = jobDisplayBranch(job);
|
|
933
|
+
const branch = name ? ` · ${name}` : '';
|
|
913
934
|
return { repo, number: num, branch, text: `${repo} #${num}${branch}`, isJira: false };
|
|
914
935
|
}
|
|
915
936
|
|
|
@@ -2696,6 +2717,7 @@ function jobMatchesReviewSearch(job, q) {
|
|
|
2696
2717
|
job.issueTitle,
|
|
2697
2718
|
job.issueUrl,
|
|
2698
2719
|
job.branchName,
|
|
2720
|
+
job.preferredBranchName,
|
|
2699
2721
|
job.prTitle,
|
|
2700
2722
|
job.prUrl,
|
|
2701
2723
|
ref?.number,
|
|
@@ -3000,6 +3022,7 @@ function updateModelLabel() {
|
|
|
3000
3022
|
? 'none selected'
|
|
3001
3023
|
: currentModel;
|
|
3002
3024
|
const combined = agentDisplayText(currentModel);
|
|
3025
|
+
const providerLabel = llmProvider === 'openrouter' ? 'OpenRouter' : 'Claude';
|
|
3003
3026
|
|
|
3004
3027
|
if (els.modelCurrent) {
|
|
3005
3028
|
els.modelCurrent.textContent = modelText;
|
|
@@ -3007,7 +3030,21 @@ function updateModelLabel() {
|
|
|
3007
3030
|
}
|
|
3008
3031
|
if (els.modelCombined) {
|
|
3009
3032
|
els.modelCombined.textContent = combined;
|
|
3010
|
-
els.modelCombined.title = combined
|
|
3033
|
+
els.modelCombined.title = `${providerLabel}: ${combined}`;
|
|
3034
|
+
}
|
|
3035
|
+
if (els.modelProvider) {
|
|
3036
|
+
els.modelProvider.textContent = providerLabel;
|
|
3037
|
+
}
|
|
3038
|
+
if (els.sidebarAgentLabel) {
|
|
3039
|
+
els.sidebarAgentLabel.textContent = providerLabel;
|
|
3040
|
+
}
|
|
3041
|
+
if (els.overviewAgentIcon) {
|
|
3042
|
+
els.overviewAgentIcon.classList.toggle('agent-pill-icon--claude', llmProvider !== 'openrouter');
|
|
3043
|
+
els.overviewAgentIcon.classList.toggle('agent-pill-icon--openrouter', llmProvider === 'openrouter');
|
|
3044
|
+
}
|
|
3045
|
+
const pillLabel = els.overviewAgentTrigger?.querySelector('.agent-pill-label');
|
|
3046
|
+
if (pillLabel) {
|
|
3047
|
+
pillLabel.textContent = providerLabel;
|
|
3011
3048
|
}
|
|
3012
3049
|
syncAgentPickerDisplay();
|
|
3013
3050
|
refreshModelSourceHint();
|
|
@@ -3513,6 +3550,16 @@ function reconcileModelForProvider(models, candidate = currentModel, storedCandi
|
|
|
3513
3550
|
* @returns {Array<{id:string,label?:string,name?:string}>}
|
|
3514
3551
|
*/
|
|
3515
3552
|
function defaultModels() {
|
|
3553
|
+
if (llmProvider === 'openrouter') {
|
|
3554
|
+
return [
|
|
3555
|
+
{ id: 'google/gemini-2.5-pro', label: 'Google: Gemini 2.5 Pro' },
|
|
3556
|
+
{ id: 'google/gemini-2.5-flash', label: 'Google: Gemini 2.5 Flash' },
|
|
3557
|
+
{ id: 'openai/gpt-4.1', label: 'OpenAI: GPT-4.1' },
|
|
3558
|
+
{ id: 'openai/gpt-4o', label: 'OpenAI: GPT-4o' },
|
|
3559
|
+
{ id: 'anthropic/claude-sonnet-4.5', label: 'Anthropic: Claude Sonnet 4.5' },
|
|
3560
|
+
{ id: 'anthropic/claude-opus-4.5', label: 'Anthropic: Claude Opus 4.5' },
|
|
3561
|
+
];
|
|
3562
|
+
}
|
|
3516
3563
|
return [
|
|
3517
3564
|
{ id: 'claude-sonnet-5', label: 'Sonnet 5' },
|
|
3518
3565
|
{ id: 'claude-opus-5', label: 'Opus 5' },
|
|
@@ -3524,16 +3571,24 @@ function defaultModels() {
|
|
|
3524
3571
|
}
|
|
3525
3572
|
|
|
3526
3573
|
/**
|
|
3527
|
-
* @param {'anthropic' | 'fallback' | string | undefined} source
|
|
3574
|
+
* @param {'anthropic' | 'fallback' | 'openrouter' | 'openrouter-fallback' | string | undefined} source
|
|
3528
3575
|
*/
|
|
3529
3576
|
function updateModelSourceHint(source) {
|
|
3530
3577
|
if (!els.settingsModelHint) return;
|
|
3531
|
-
if (source === '
|
|
3578
|
+
if (source === 'openrouter') {
|
|
3579
|
+
modelSourceHintBase =
|
|
3580
|
+
'OpenRouter model for agent runs. List loaded from OpenRouter Models API.';
|
|
3581
|
+
} else if (source === 'openrouter-fallback') {
|
|
3582
|
+
modelSourceHintBase =
|
|
3583
|
+
'OpenRouter model for agent runs. Showing a short fallback list (live catalog unavailable).';
|
|
3584
|
+
} else if (source === 'anthropic') {
|
|
3532
3585
|
modelSourceHintBase =
|
|
3533
3586
|
'Claude model for agent runs. List loaded from Anthropic Models API.';
|
|
3534
3587
|
} else if (source === 'fallback') {
|
|
3535
3588
|
modelSourceHintBase =
|
|
3536
3589
|
'Claude model for agent runs. Showing curated Claude Code models (live list unavailable — set an API key or use claude auth login).';
|
|
3590
|
+
} else if (llmProvider === 'openrouter') {
|
|
3591
|
+
modelSourceHintBase = 'OpenRouter model for agent runs.';
|
|
3537
3592
|
} else {
|
|
3538
3593
|
modelSourceHintBase = 'Claude model for agent runs.';
|
|
3539
3594
|
}
|
|
@@ -3603,6 +3658,7 @@ async function fetchModels(opts = {}) {
|
|
|
3603
3658
|
}
|
|
3604
3659
|
const query = new URLSearchParams();
|
|
3605
3660
|
if (refresh) query.set('refresh', '1');
|
|
3661
|
+
if (llmProvider) query.set('provider', llmProvider);
|
|
3606
3662
|
try {
|
|
3607
3663
|
const res = await fetch(`/api/models?${query.toString()}`);
|
|
3608
3664
|
if (!res.ok) {
|
|
@@ -3745,6 +3801,13 @@ function claudeStatusText(cfg) {
|
|
|
3745
3801
|
}
|
|
3746
3802
|
}
|
|
3747
3803
|
|
|
3804
|
+
function openrouterStatusText(cfg) {
|
|
3805
|
+
if (cfg.openrouterAuthOk) {
|
|
3806
|
+
return 'Authenticated via OPENROUTER_API_KEY';
|
|
3807
|
+
}
|
|
3808
|
+
return 'Not authenticated — add an OpenRouter API key';
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3748
3811
|
function fillAuthSettings(cfg) {
|
|
3749
3812
|
if (els.settingsGhStatus) {
|
|
3750
3813
|
els.settingsGhStatus.textContent = ghStatusText(cfg);
|
|
@@ -3803,6 +3866,25 @@ function fillAuthSettings(cfg) {
|
|
|
3803
3866
|
? '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.'
|
|
3804
3867
|
: '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.';
|
|
3805
3868
|
}
|
|
3869
|
+
|
|
3870
|
+
if (els.settingsOpenrouterStatus) {
|
|
3871
|
+
els.settingsOpenrouterStatus.textContent = openrouterStatusText(cfg);
|
|
3872
|
+
els.settingsOpenrouterStatus.className = cfg.openrouterAuthOk
|
|
3873
|
+
? 'auth-status ok'
|
|
3874
|
+
: 'auth-status err';
|
|
3875
|
+
}
|
|
3876
|
+
fillSecretInput(
|
|
3877
|
+
els.settingsOpenrouterKey,
|
|
3878
|
+
els.settingsOpenrouterKeyClear,
|
|
3879
|
+
Boolean(cfg.openrouterApiKeySet),
|
|
3880
|
+
cfg.openrouterApiKeyMasked,
|
|
3881
|
+
'Paste an OpenRouter API key'
|
|
3882
|
+
);
|
|
3883
|
+
if (els.settingsOpenrouterKeyHint) {
|
|
3884
|
+
els.settingsOpenrouterKeyHint.innerHTML = cfg.openrouterApiKeySet
|
|
3885
|
+
? 'API key stored in <code>.acdev/.env</code> as <code>OPENROUTER_API_KEY</code> (not committed). Leave blank to keep.'
|
|
3886
|
+
: 'From openrouter.ai → Keys. Required when the LLM provider is OpenRouter. Stored as <code>OPENROUTER_API_KEY</code>.';
|
|
3887
|
+
}
|
|
3806
3888
|
}
|
|
3807
3889
|
|
|
3808
3890
|
function fillSettingsForm(cfg) {
|
|
@@ -3811,6 +3893,7 @@ function fillSettingsForm(cfg) {
|
|
|
3811
3893
|
fillAuthSettings(cfg);
|
|
3812
3894
|
|
|
3813
3895
|
updateTicketSourceUI(cfg.ticketSource === 'jira' ? 'jira' : 'github');
|
|
3896
|
+
updateLlmProviderUI(cfg.llmProvider === 'openrouter' ? 'openrouter' : 'claude');
|
|
3814
3897
|
|
|
3815
3898
|
if (els.settingsJiraBaseUrl) {
|
|
3816
3899
|
els.settingsJiraBaseUrl.value = cfg.jiraBaseUrl || '';
|
|
@@ -4037,6 +4120,23 @@ function updateTicketSourceUI(source) {
|
|
|
4037
4120
|
}
|
|
4038
4121
|
}
|
|
4039
4122
|
|
|
4123
|
+
/**
|
|
4124
|
+
* @param {'claude' | 'openrouter'} provider
|
|
4125
|
+
*/
|
|
4126
|
+
function updateLlmProviderUI(provider) {
|
|
4127
|
+
llmProvider = provider === 'openrouter' ? 'openrouter' : 'claude';
|
|
4128
|
+
|
|
4129
|
+
for (const toggle of [els.settingsLlmProvider, els.overviewLlmProvider]) {
|
|
4130
|
+
if (!toggle) continue;
|
|
4131
|
+
toggle.querySelectorAll('.source-btn').forEach((btn) => {
|
|
4132
|
+
const active = btn.dataset.provider === llmProvider;
|
|
4133
|
+
btn.classList.toggle('active', active);
|
|
4134
|
+
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
4135
|
+
});
|
|
4136
|
+
}
|
|
4137
|
+
updateModelLabel();
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4040
4140
|
/**
|
|
4041
4141
|
* @param {'github' | 'jira'} next
|
|
4042
4142
|
*/
|
|
@@ -4058,6 +4158,26 @@ async function saveTicketSource(next) {
|
|
|
4058
4158
|
}
|
|
4059
4159
|
}
|
|
4060
4160
|
|
|
4161
|
+
async function saveLlmProvider(next) {
|
|
4162
|
+
const provider = next === 'openrouter' ? 'openrouter' : 'claude';
|
|
4163
|
+
updateLlmProviderUI(provider);
|
|
4164
|
+
clearAvailableModels();
|
|
4165
|
+
try {
|
|
4166
|
+
const res = await fetch('/api/config', {
|
|
4167
|
+
method: 'PATCH',
|
|
4168
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4169
|
+
body: JSON.stringify({ llmProvider: provider }),
|
|
4170
|
+
});
|
|
4171
|
+
const data = await readJson(res);
|
|
4172
|
+
if (res.ok) {
|
|
4173
|
+
applyConfigSnapshot({ ...appConfig, ...data });
|
|
4174
|
+
await fetchModels({ refresh: true, reconcile: true });
|
|
4175
|
+
}
|
|
4176
|
+
} catch {
|
|
4177
|
+
void fetchModels({ refresh: true, reconcile: true });
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
|
|
4061
4181
|
function updateTimeoutMsHint(minutes) {
|
|
4062
4182
|
if (!els.settingsTimeoutMs) return;
|
|
4063
4183
|
const m = Number(minutes);
|
|
@@ -4085,6 +4205,7 @@ function setOverviewLlmFeedback(message, kind = 'ok') {
|
|
|
4085
4205
|
*/
|
|
4086
4206
|
function fillOverviewLlmControls(cfg) {
|
|
4087
4207
|
if (!cfg) return;
|
|
4208
|
+
updateLlmProviderUI(cfg.llmProvider === 'openrouter' ? 'openrouter' : 'claude');
|
|
4088
4209
|
syncAllModelComboboxValues(currentModel);
|
|
4089
4210
|
}
|
|
4090
4211
|
|
|
@@ -4132,6 +4253,7 @@ function applyConfigSnapshot(data) {
|
|
|
4132
4253
|
updateModelLabel();
|
|
4133
4254
|
fillOverviewLlmControls(data);
|
|
4134
4255
|
updateTicketSourceUI(data?.ticketSource === 'jira' ? 'jira' : 'github');
|
|
4256
|
+
updateLlmProviderUI(data?.llmProvider === 'openrouter' ? 'openrouter' : 'claude');
|
|
4135
4257
|
if (els.repoName) {
|
|
4136
4258
|
els.repoName.textContent = data?.repoName || 'local repo';
|
|
4137
4259
|
}
|
|
@@ -4178,6 +4300,7 @@ function readSettingsForm() {
|
|
|
4178
4300
|
testCommand: testTrimmed === '' ? null : testTrimmed,
|
|
4179
4301
|
allowedTools,
|
|
4180
4302
|
ticketSource,
|
|
4303
|
+
llmProvider,
|
|
4181
4304
|
};
|
|
4182
4305
|
|
|
4183
4306
|
const ghToken = secretPatchValue(els.settingsGhToken);
|
|
@@ -4186,6 +4309,8 @@ function readSettingsForm() {
|
|
|
4186
4309
|
if (anthropicApiKey !== undefined) patch.anthropicApiKey = anthropicApiKey;
|
|
4187
4310
|
const claudeOauthToken = secretPatchValue(els.settingsClaudeOauth);
|
|
4188
4311
|
if (claudeOauthToken !== undefined) patch.claudeOauthToken = claudeOauthToken;
|
|
4312
|
+
const openrouterApiKey = secretPatchValue(els.settingsOpenrouterKey);
|
|
4313
|
+
if (openrouterApiKey !== undefined) patch.openrouterApiKey = openrouterApiKey;
|
|
4189
4314
|
|
|
4190
4315
|
if (ticketSource === 'jira' || els.settingsJiraBaseUrl?.value) {
|
|
4191
4316
|
patch.jiraBaseUrl = els.settingsJiraBaseUrl?.value?.trim() || '';
|
|
@@ -4353,6 +4478,12 @@ bindSecretClear(
|
|
|
4353
4478
|
els.settingsClaudeOauthHint,
|
|
4354
4479
|
'Saved CLAUDE_CODE_OAUTH_TOKEN will be removed when you click Save settings.'
|
|
4355
4480
|
);
|
|
4481
|
+
bindSecretClear(
|
|
4482
|
+
els.settingsOpenrouterKeyClear,
|
|
4483
|
+
els.settingsOpenrouterKey,
|
|
4484
|
+
els.settingsOpenrouterKeyHint,
|
|
4485
|
+
'Saved OPENROUTER_API_KEY will be removed when you click Save settings.'
|
|
4486
|
+
);
|
|
4356
4487
|
|
|
4357
4488
|
|
|
4358
4489
|
document.addEventListener('click', (ev) => {
|
|
@@ -4445,18 +4576,29 @@ els.issueUrls?.addEventListener('keydown', (ev) => {
|
|
|
4445
4576
|
els.addBtn?.click();
|
|
4446
4577
|
});
|
|
4447
4578
|
|
|
4579
|
+
els.preferredBranch?.addEventListener('keydown', (ev) => {
|
|
4580
|
+
if (ev.key !== 'Enter' || ev.shiftKey || ev.metaKey || ev.ctrlKey || ev.altKey) return;
|
|
4581
|
+
ev.preventDefault();
|
|
4582
|
+
els.addBtn?.click();
|
|
4583
|
+
});
|
|
4584
|
+
|
|
4448
4585
|
els.addBtn.addEventListener('click', async () => {
|
|
4449
4586
|
els.enqueueFeedback.classList.add('hidden');
|
|
4450
4587
|
const text = els.issueUrls.value.trim();
|
|
4451
4588
|
if (!text) return;
|
|
4452
4589
|
|
|
4453
4590
|
const urls = splitIssueUrls(text);
|
|
4591
|
+
const branchName = els.preferredBranch?.value?.trim() || '';
|
|
4454
4592
|
|
|
4455
4593
|
try {
|
|
4456
4594
|
const res = await fetch('/api/issues', {
|
|
4457
4595
|
method: 'POST',
|
|
4458
4596
|
headers: { 'Content-Type': 'application/json' },
|
|
4459
|
-
body: JSON.stringify({
|
|
4597
|
+
body: JSON.stringify({
|
|
4598
|
+
urls,
|
|
4599
|
+
ticketSource,
|
|
4600
|
+
...(branchName ? { branchName } : {}),
|
|
4601
|
+
}),
|
|
4460
4602
|
});
|
|
4461
4603
|
const data = await res.json();
|
|
4462
4604
|
if (!res.ok) {
|
|
@@ -4467,6 +4609,7 @@ els.addBtn.addEventListener('click', async () => {
|
|
|
4467
4609
|
return;
|
|
4468
4610
|
}
|
|
4469
4611
|
els.issueUrls.value = '';
|
|
4612
|
+
if (els.preferredBranch) els.preferredBranch.value = '';
|
|
4470
4613
|
const parts = [];
|
|
4471
4614
|
if (data.jobs?.length) parts.push(`Added ${data.jobs.length}`);
|
|
4472
4615
|
if (data.skipped?.length) {
|
|
@@ -4501,6 +4644,18 @@ function handleTicketSourceToggleClick(e) {
|
|
|
4501
4644
|
els.settingsTicketSource?.addEventListener('click', handleTicketSourceToggleClick);
|
|
4502
4645
|
els.overviewTicketSource?.addEventListener('click', handleTicketSourceToggleClick);
|
|
4503
4646
|
|
|
4647
|
+
function handleLlmProviderToggleClick(e) {
|
|
4648
|
+
const btn = e.target.closest('.source-btn');
|
|
4649
|
+
if (!btn?.dataset.provider) return;
|
|
4650
|
+
const next = btn.dataset.provider === 'openrouter' ? 'openrouter' : 'claude';
|
|
4651
|
+
if (next === llmProvider) return;
|
|
4652
|
+
e.stopPropagation();
|
|
4653
|
+
void saveLlmProvider(next);
|
|
4654
|
+
}
|
|
4655
|
+
|
|
4656
|
+
els.settingsLlmProvider?.addEventListener('click', handleLlmProviderToggleClick);
|
|
4657
|
+
els.overviewLlmProvider?.addEventListener('click', handleLlmProviderToggleClick);
|
|
4658
|
+
|
|
4504
4659
|
els.enqueueJiraSettingsLink?.addEventListener('click', (e) => {
|
|
4505
4660
|
e.preventDefault();
|
|
4506
4661
|
setView('settings');
|
package/public/index.html
CHANGED
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
<div class="sidebar-footer">
|
|
73
73
|
<div class="sidebar-divider"></div>
|
|
74
74
|
<div class="model-block">
|
|
75
|
-
<div class="model-label">Agent</div>
|
|
75
|
+
<div class="model-label" id="sidebar-agent-label">Agent</div>
|
|
76
76
|
<div class="model-meta-row">
|
|
77
77
|
<span class="model-combined" id="model-combined">—</span>
|
|
78
78
|
</div>
|
|
@@ -138,6 +138,24 @@
|
|
|
138
138
|
or a browse link. Press <kbd class="enqueue-kbd">Enter</kbd> to add.
|
|
139
139
|
</p>
|
|
140
140
|
|
|
141
|
+
<div class="enqueue-branch-field">
|
|
142
|
+
<div class="enqueue-branch-label-row">
|
|
143
|
+
<label class="field-label" for="preferred-branch">Branch name</label>
|
|
144
|
+
<span class="enqueue-branch-optional">Optional</span>
|
|
145
|
+
</div>
|
|
146
|
+
<input
|
|
147
|
+
id="preferred-branch"
|
|
148
|
+
class="input enqueue-branch-input"
|
|
149
|
+
type="text"
|
|
150
|
+
spellcheck="false"
|
|
151
|
+
autocomplete="off"
|
|
152
|
+
placeholder="your-branch-name"
|
|
153
|
+
>
|
|
154
|
+
<p class="enqueue-input-hint">
|
|
155
|
+
Leave empty to keep feat/fix + ticket title. If set, that name is used instead.
|
|
156
|
+
</p>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
141
159
|
<div class="enqueue-info-banner" id="enqueue-info-banner" role="note">
|
|
142
160
|
<svg class="enqueue-info-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
143
161
|
<circle cx="12" cy="12" r="10"/>
|
|
@@ -172,6 +190,10 @@
|
|
|
172
190
|
<svg class="agent-pill-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
|
|
173
191
|
</button>
|
|
174
192
|
<div class="agent-picker-panel" id="overview-agent-panel" role="dialog" aria-label="Choose agent model" aria-hidden="true">
|
|
193
|
+
<div class="source-toggle source-toggle--compact agent-picker-provider" id="overview-llm-provider" role="group" aria-label="LLM provider">
|
|
194
|
+
<button type="button" class="source-btn active" data-provider="claude">Claude</button>
|
|
195
|
+
<button type="button" class="source-btn" data-provider="openrouter">OpenRouter</button>
|
|
196
|
+
</div>
|
|
175
197
|
<label class="sr-only" for="overview-agent-search">Search models</label>
|
|
176
198
|
<input
|
|
177
199
|
id="overview-agent-search"
|
|
@@ -530,6 +552,21 @@
|
|
|
530
552
|
</div>
|
|
531
553
|
</div>
|
|
532
554
|
</div>
|
|
555
|
+
|
|
556
|
+
<div class="card settings-card">
|
|
557
|
+
<div class="rules-heading">OpenRouter authentication</div>
|
|
558
|
+
<p id="settings-openrouter-status" class="auth-status" role="status">Checking OpenRouter auth…</p>
|
|
559
|
+
<div class="settings-grid">
|
|
560
|
+
<div class="settings-field settings-field-full">
|
|
561
|
+
<label class="field-label" for="settings-openrouter-key">OpenRouter API key</label>
|
|
562
|
+
<input id="settings-openrouter-key" class="input" type="password" name="openrouterApiKey" autocomplete="new-password" placeholder="Leave blank to keep existing">
|
|
563
|
+
<button type="button" class="auth-clear hidden" id="settings-openrouter-key-clear">Clear saved API key</button>
|
|
564
|
+
<p class="field-hint" id="settings-openrouter-key-hint">
|
|
565
|
+
From openrouter.ai → Keys. Required when the LLM provider is OpenRouter. Stored as <code>OPENROUTER_API_KEY</code>.
|
|
566
|
+
</p>
|
|
567
|
+
</div>
|
|
568
|
+
</div>
|
|
569
|
+
</div>
|
|
533
570
|
</div>
|
|
534
571
|
</div>
|
|
535
572
|
|
|
@@ -625,6 +662,14 @@
|
|
|
625
662
|
Changes save to <code>.acdev/config.json</code> and apply to the next job — no restart needed.
|
|
626
663
|
</p>
|
|
627
664
|
<div class="card settings-card">
|
|
665
|
+
<div class="settings-field settings-field-full">
|
|
666
|
+
<div class="field-label">LLM provider</div>
|
|
667
|
+
<div class="source-toggle" id="settings-llm-provider" role="group" aria-label="LLM provider">
|
|
668
|
+
<button type="button" class="source-btn active" data-provider="claude">Claude</button>
|
|
669
|
+
<button type="button" class="source-btn" data-provider="openrouter">OpenRouter</button>
|
|
670
|
+
</div>
|
|
671
|
+
<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
|
+
</div>
|
|
628
673
|
<div class="settings-grid">
|
|
629
674
|
<div class="settings-field">
|
|
630
675
|
<label class="field-label" for="settings-base-branch">Base branch</label>
|
|
@@ -664,7 +709,7 @@
|
|
|
664
709
|
></ul>
|
|
665
710
|
</div>
|
|
666
711
|
</div>
|
|
667
|
-
<p class="field-hint" id="settings-model-hint">
|
|
712
|
+
<p class="field-hint" id="settings-model-hint">Model for agent runs.</p>
|
|
668
713
|
</div>
|
|
669
714
|
|
|
670
715
|
<div class="settings-field">
|
package/public/styles.css
CHANGED
|
@@ -459,6 +459,38 @@ a { color: var(--primary); text-underline-offset: 3px; }
|
|
|
459
459
|
color: var(--text-muted);
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
+
.enqueue-branch-field {
|
|
463
|
+
display: flex;
|
|
464
|
+
flex-direction: column;
|
|
465
|
+
gap: 6px;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.enqueue-branch-field .field-label {
|
|
469
|
+
margin-bottom: 0;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.enqueue-branch-field .enqueue-input-hint {
|
|
473
|
+
margin: 0;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.enqueue-branch-label-row {
|
|
477
|
+
display: flex;
|
|
478
|
+
align-items: baseline;
|
|
479
|
+
justify-content: space-between;
|
|
480
|
+
gap: 8px;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.enqueue-branch-optional {
|
|
484
|
+
font-size: 11px;
|
|
485
|
+
font-weight: 500;
|
|
486
|
+
color: var(--text-muted);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.enqueue-branch-input {
|
|
490
|
+
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
491
|
+
font-size: 13px;
|
|
492
|
+
}
|
|
493
|
+
|
|
462
494
|
.enqueue-kbd {
|
|
463
495
|
display: inline-block;
|
|
464
496
|
padding: 1px 6px;
|
|
@@ -556,6 +588,10 @@ a { color: var(--primary); text-underline-offset: 3px; }
|
|
|
556
588
|
background: linear-gradient(145deg, #e8a87c 0%, #d97706 100%);
|
|
557
589
|
}
|
|
558
590
|
|
|
591
|
+
.agent-pill-icon--openrouter {
|
|
592
|
+
background: linear-gradient(145deg, #7c9cff 0%, #4f46e5 100%);
|
|
593
|
+
}
|
|
594
|
+
|
|
559
595
|
.agent-pill-body {
|
|
560
596
|
flex: 1;
|
|
561
597
|
min-width: 0;
|
|
@@ -604,7 +640,7 @@ a { color: var(--primary); text-underline-offset: 3px; }
|
|
|
604
640
|
display: flex;
|
|
605
641
|
flex-direction: column;
|
|
606
642
|
width: min(360px, calc(100vw - 48px));
|
|
607
|
-
max-height:
|
|
643
|
+
max-height: 360px;
|
|
608
644
|
background: var(--surface);
|
|
609
645
|
border: 1px solid var(--border);
|
|
610
646
|
border-radius: 10px;
|
|
@@ -681,6 +717,16 @@ a { color: var(--primary); text-underline-offset: 3px; }
|
|
|
681
717
|
background: transparent;
|
|
682
718
|
}
|
|
683
719
|
|
|
720
|
+
.agent-picker-provider {
|
|
721
|
+
margin: 10px 10px 0;
|
|
722
|
+
max-width: none;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
.agent-picker-provider .source-btn {
|
|
726
|
+
padding: 7px 10px;
|
|
727
|
+
font-size: 12px;
|
|
728
|
+
}
|
|
729
|
+
|
|
684
730
|
.agent-picker-search {
|
|
685
731
|
flex-shrink: 0;
|
|
686
732
|
min-height: 36px;
|