acdev 1.0.1 → 1.0.3
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 +8 -2
- package/README.md +59 -21
- package/bin/acdev.js +4 -24
- package/package.json +1 -1
- package/public/app.js +551 -129
- package/public/index.html +195 -63
- package/public/styles.css +81 -21
- package/src/afterPrRules.js +63 -8
- package/src/claude-auth.js +30 -0
- package/src/config.js +128 -88
- package/src/env.js +1 -1
- package/src/gh-auth.js +123 -6
- package/src/jira.js +171 -0
- package/src/models.js +366 -0
- package/src/server.js +44 -11
package/public/app.js
CHANGED
|
@@ -108,6 +108,10 @@ let reviewSearch = loadReviewSearch();
|
|
|
108
108
|
let activeInlineCommentKey = null;
|
|
109
109
|
/** @type {string | null} id of line comment currently being edited */
|
|
110
110
|
let editingCommentId = null;
|
|
111
|
+
/** @type {'inline' | 'pending' | null} where the edit form should render */
|
|
112
|
+
let editingCommentSurface = null;
|
|
113
|
+
/** Original body when edit started — restored on Cancel after live sync. */
|
|
114
|
+
let editingCommentOriginalBody = null;
|
|
111
115
|
/** @type {Record<string, {
|
|
112
116
|
* generalComment: string,
|
|
113
117
|
* lineComments: Array<{ id: string, path: string, line: number, side: 'LEFT' | 'RIGHT', body: string }>,
|
|
@@ -120,10 +124,13 @@ let reviewDrafts = {};
|
|
|
120
124
|
let reviewFileLists = {};
|
|
121
125
|
/** @type {Record<string, Set<string>>} excluded paths per job while reviewing */
|
|
122
126
|
let reviewExcludedByJob = {};
|
|
123
|
-
let modelSaving = false;
|
|
124
127
|
let settingsSaving = false;
|
|
128
|
+
/** @type {'ticket' | 'auth' | 'rules' | 'config'} */
|
|
129
|
+
let settingsTab = 'ticket';
|
|
125
130
|
/** @type {string} */
|
|
126
131
|
let currentModel = 'claude-sonnet-5';
|
|
132
|
+
/** @type {Array<{id:string,label?:string,name?:string}>} */
|
|
133
|
+
let availableModels = [];
|
|
127
134
|
/** @type {'github' | 'jira'} */
|
|
128
135
|
let ticketSource = 'github';
|
|
129
136
|
/** @type {{
|
|
@@ -143,6 +150,19 @@ let ticketSource = 'github';
|
|
|
143
150
|
* jiraApiTokenMasked?: string | null,
|
|
144
151
|
* jiraConfigured?: boolean,
|
|
145
152
|
* jiraPrLinkPhrase?: string,
|
|
153
|
+
* ghAuthOk?: boolean,
|
|
154
|
+
* ghInstalled?: boolean,
|
|
155
|
+
* ghTokenSet?: boolean,
|
|
156
|
+
* ghTokenMasked?: string | null,
|
|
157
|
+
* ghAuthMethod?: 'token' | 'gh-login' | null,
|
|
158
|
+
* originRemoteUrl?: string | null,
|
|
159
|
+
* originRemoteSsh?: boolean,
|
|
160
|
+
* claudeAuthOk?: boolean,
|
|
161
|
+
* claudeAuthMethod?: string,
|
|
162
|
+
* anthropicApiKeySet?: boolean,
|
|
163
|
+
* anthropicApiKeyMasked?: string | null,
|
|
164
|
+
* claudeOauthTokenSet?: boolean,
|
|
165
|
+
* claudeOauthTokenMasked?: string | null,
|
|
146
166
|
* }} */
|
|
147
167
|
let appConfig = {};
|
|
148
168
|
|
|
@@ -153,7 +173,7 @@ const els = {
|
|
|
153
173
|
repoName: document.getElementById('repo-name'),
|
|
154
174
|
repoBranch: document.getElementById('repo-branch'),
|
|
155
175
|
ticketSourceMeta: document.getElementById('ticket-source-meta'),
|
|
156
|
-
|
|
176
|
+
modelCurrent: document.getElementById('model-current'),
|
|
157
177
|
navBadgeOverview: document.getElementById('nav-badge-overview'),
|
|
158
178
|
navBadgeRuns: document.getElementById('nav-badge-runs'),
|
|
159
179
|
navBadgeReview: document.getElementById('nav-badge-review'),
|
|
@@ -217,6 +237,7 @@ const els = {
|
|
|
217
237
|
settingsForm: document.getElementById('settings-form'),
|
|
218
238
|
settingsBaseBranch: document.getElementById('settings-base-branch'),
|
|
219
239
|
settingsModel: document.getElementById('settings-model'),
|
|
240
|
+
settingsModelHint: document.getElementById('settings-model-hint'),
|
|
220
241
|
settingsMaxTurns: document.getElementById('settings-max-turns'),
|
|
221
242
|
settingsTimeout: document.getElementById('settings-timeout'),
|
|
222
243
|
settingsTimeoutMs: document.getElementById('settings-timeout-ms'),
|
|
@@ -234,13 +255,30 @@ const els = {
|
|
|
234
255
|
jiraTestBtn: document.getElementById('jira-test-btn'),
|
|
235
256
|
jiraStatus: document.getElementById('jira-status'),
|
|
236
257
|
settingsJiraRuleEnabled: document.getElementById('settings-jira-rule-enabled'),
|
|
258
|
+
settingsJiraRuleAction: document.getElementById('settings-jira-rule-action'),
|
|
237
259
|
settingsJiraRuleStatus: document.getElementById('settings-jira-rule-status'),
|
|
260
|
+
settingsJiraRuleStatusField: document.getElementById('settings-jira-rule-status-field'),
|
|
261
|
+
settingsJiraRuleLabel: document.getElementById('settings-jira-rule-label'),
|
|
262
|
+
settingsJiraRuleLabelField: document.getElementById('settings-jira-rule-label-field'),
|
|
238
263
|
settingsGithubRuleEnabled: document.getElementById('settings-github-rule-enabled'),
|
|
239
264
|
settingsGithubRuleAction: document.getElementById('settings-github-rule-action'),
|
|
265
|
+
settingsGithubRuleStatus: document.getElementById('settings-github-rule-status'),
|
|
266
|
+
settingsGithubRuleStatusField: document.getElementById('settings-github-rule-status-field'),
|
|
240
267
|
settingsGithubRuleLabel: document.getElementById('settings-github-rule-label'),
|
|
241
268
|
settingsGithubRuleLabelField: document.getElementById('settings-github-rule-label-field'),
|
|
242
|
-
|
|
243
|
-
|
|
269
|
+
settingsGhStatus: document.getElementById('settings-gh-status'),
|
|
270
|
+
settingsGhOrigin: document.getElementById('settings-gh-origin'),
|
|
271
|
+
settingsGhToken: document.getElementById('settings-gh-token'),
|
|
272
|
+
settingsGhTokenClear: document.getElementById('settings-gh-token-clear'),
|
|
273
|
+
settingsGhTokenHint: document.getElementById('settings-gh-token-hint'),
|
|
274
|
+
settingsClaudeStatus: document.getElementById('settings-claude-status'),
|
|
275
|
+
settingsAnthropicKey: document.getElementById('settings-anthropic-key'),
|
|
276
|
+
settingsAnthropicKeyClear: document.getElementById('settings-anthropic-key-clear'),
|
|
277
|
+
settingsAnthropicKeyHint: document.getElementById('settings-anthropic-key-hint'),
|
|
278
|
+
settingsClaudeOauth: document.getElementById('settings-claude-oauth'),
|
|
279
|
+
settingsClaudeOauthClear: document.getElementById('settings-claude-oauth-clear'),
|
|
280
|
+
settingsClaudeOauthHint: document.getElementById('settings-claude-oauth-hint'),
|
|
281
|
+
settingsTabs: document.getElementById('settings-tabs'),
|
|
244
282
|
};
|
|
245
283
|
|
|
246
284
|
function loadDismissed() {
|
|
@@ -447,6 +485,8 @@ function clearReviewDraft(jobId) {
|
|
|
447
485
|
delete reviewDrafts[jobId];
|
|
448
486
|
activeInlineCommentKey = null;
|
|
449
487
|
editingCommentId = null;
|
|
488
|
+
editingCommentSurface = null;
|
|
489
|
+
editingCommentOriginalBody = null;
|
|
450
490
|
try {
|
|
451
491
|
sessionStorage.removeItem(reviewDraftStorageKey(jobId));
|
|
452
492
|
} catch {
|
|
@@ -454,6 +494,86 @@ function clearReviewDraft(jobId) {
|
|
|
454
494
|
}
|
|
455
495
|
}
|
|
456
496
|
|
|
497
|
+
/** Clear in-progress line-comment edit state. */
|
|
498
|
+
function clearCommentEditState() {
|
|
499
|
+
editingCommentId = null;
|
|
500
|
+
editingCommentSurface = null;
|
|
501
|
+
editingCommentOriginalBody = null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* @param {string} jobId
|
|
506
|
+
* @param {string} commentId
|
|
507
|
+
* @param {string} body
|
|
508
|
+
*/
|
|
509
|
+
function updateLineCommentBody(jobId, commentId, body) {
|
|
510
|
+
const d = loadReviewDraft(jobId);
|
|
511
|
+
const target = d.lineComments.find((x) => x.id === commentId);
|
|
512
|
+
if (!target) return;
|
|
513
|
+
target.body = body;
|
|
514
|
+
saveReviewDraft(jobId);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Refresh the non-active edit surface so both views stay in sync.
|
|
519
|
+
* @param {string} jobId
|
|
520
|
+
*/
|
|
521
|
+
function mirrorCommentEditToOtherSurface(jobId) {
|
|
522
|
+
if (editingCommentSurface === 'inline') {
|
|
523
|
+
renderPendingCommentList(jobId);
|
|
524
|
+
syncSubmitReviewButton(jobId);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (editingCommentSurface === 'pending') {
|
|
528
|
+
const job = jobsById[jobId];
|
|
529
|
+
if (job) {
|
|
530
|
+
const diff =
|
|
531
|
+
job.status === 'awaiting_review'
|
|
532
|
+
? filterDiffForReviewJob(job)
|
|
533
|
+
: job.diff || '';
|
|
534
|
+
renderDiff(diff, {
|
|
535
|
+
interactive: job.status === 'awaiting_review',
|
|
536
|
+
jobId,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
syncSubmitReviewButton(jobId);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Commit or discard an in-progress comment edit.
|
|
545
|
+
* @param {string} jobId
|
|
546
|
+
* @param {string} commentId
|
|
547
|
+
* @param {string | null} nextBody null = cancel / restore original
|
|
548
|
+
*/
|
|
549
|
+
function finishCommentEdit(jobId, commentId, nextBody) {
|
|
550
|
+
if (nextBody == null) {
|
|
551
|
+
if (editingCommentOriginalBody != null) {
|
|
552
|
+
updateLineCommentBody(jobId, commentId, editingCommentOriginalBody);
|
|
553
|
+
}
|
|
554
|
+
} else {
|
|
555
|
+
updateLineCommentBody(jobId, commentId, nextBody);
|
|
556
|
+
}
|
|
557
|
+
clearCommentEditState();
|
|
558
|
+
refreshReviewCommentUi(jobId);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Start editing a pending line comment on a specific surface.
|
|
563
|
+
* @param {string} commentId
|
|
564
|
+
* @param {'inline' | 'pending'} surface
|
|
565
|
+
* @param {string} jobId
|
|
566
|
+
*/
|
|
567
|
+
function startCommentEdit(commentId, surface, jobId) {
|
|
568
|
+
const draft = loadReviewDraft(jobId);
|
|
569
|
+
const target = draft.lineComments.find((x) => x.id === commentId);
|
|
570
|
+
editingCommentId = commentId;
|
|
571
|
+
editingCommentSurface = surface;
|
|
572
|
+
editingCommentOriginalBody = target ? target.body : '';
|
|
573
|
+
activeInlineCommentKey = null;
|
|
574
|
+
refreshReviewCommentUi(jobId);
|
|
575
|
+
}
|
|
576
|
+
|
|
457
577
|
/**
|
|
458
578
|
* Re-render diff + pending list after draft mutation.
|
|
459
579
|
* @param {string} jobId
|
|
@@ -480,6 +600,7 @@ function refreshReviewCommentUi(jobId) {
|
|
|
480
600
|
* saveLabel?: string,
|
|
481
601
|
* onSave: (body: string) => void,
|
|
482
602
|
* onCancel: () => void,
|
|
603
|
+
* onInput?: (body: string) => void,
|
|
483
604
|
* }} opts
|
|
484
605
|
*/
|
|
485
606
|
function buildCommentEditForm(opts) {
|
|
@@ -490,6 +611,9 @@ function buildCommentEditForm(opts) {
|
|
|
490
611
|
ta.rows = 3;
|
|
491
612
|
ta.value = opts.initialBody;
|
|
492
613
|
ta.placeholder = 'Edit comment…';
|
|
614
|
+
if (opts.onInput) {
|
|
615
|
+
ta.addEventListener('input', () => opts.onInput(ta.value));
|
|
616
|
+
}
|
|
493
617
|
const actions = document.createElement('div');
|
|
494
618
|
actions.className = 'diff-inline-actions';
|
|
495
619
|
const save = document.createElement('button');
|
|
@@ -514,6 +638,7 @@ function buildCommentEditForm(opts) {
|
|
|
514
638
|
ta.focus();
|
|
515
639
|
const len = ta.value.length;
|
|
516
640
|
ta.setSelectionRange(len, len);
|
|
641
|
+
ta.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
517
642
|
});
|
|
518
643
|
return form;
|
|
519
644
|
}
|
|
@@ -1317,7 +1442,7 @@ function renderDiff(diff, opts = {}) {
|
|
|
1317
1442
|
plus.addEventListener('click', (e) => {
|
|
1318
1443
|
e.stopPropagation();
|
|
1319
1444
|
activeInlineCommentKey = lineCommentKey(path, line, side);
|
|
1320
|
-
|
|
1445
|
+
clearCommentEditState();
|
|
1321
1446
|
renderDiff(diff, opts);
|
|
1322
1447
|
renderPendingCommentList(jobId);
|
|
1323
1448
|
syncSubmitReviewButton(jobId);
|
|
@@ -1327,7 +1452,7 @@ function renderDiff(diff, opts = {}) {
|
|
|
1327
1452
|
cell.addEventListener('click', (e) => {
|
|
1328
1453
|
if (e.target.closest('button, textarea, a')) return;
|
|
1329
1454
|
activeInlineCommentKey = lineCommentKey(path, line, side);
|
|
1330
|
-
|
|
1455
|
+
clearCommentEditState();
|
|
1331
1456
|
renderDiff(diff, opts);
|
|
1332
1457
|
renderPendingCommentList(jobId);
|
|
1333
1458
|
syncSubmitReviewButton(jobId);
|
|
@@ -1382,22 +1507,16 @@ function renderDiff(diff, opts = {}) {
|
|
|
1382
1507
|
thread.appendChild(meta);
|
|
1383
1508
|
|
|
1384
1509
|
for (const c of existing) {
|
|
1385
|
-
if (editingCommentId === c.id) {
|
|
1510
|
+
if (editingCommentId === c.id && editingCommentSurface === 'inline') {
|
|
1386
1511
|
thread.appendChild(
|
|
1387
1512
|
buildCommentEditForm({
|
|
1388
1513
|
initialBody: c.body,
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
if (target) target.body = body;
|
|
1393
|
-
saveReviewDraft(jobId);
|
|
1394
|
-
editingCommentId = null;
|
|
1395
|
-
refreshReviewCommentUi(jobId);
|
|
1396
|
-
},
|
|
1397
|
-
onCancel: () => {
|
|
1398
|
-
editingCommentId = null;
|
|
1399
|
-
refreshReviewCommentUi(jobId);
|
|
1514
|
+
onInput: (body) => {
|
|
1515
|
+
updateLineCommentBody(jobId, c.id, body);
|
|
1516
|
+
mirrorCommentEditToOtherSurface(jobId);
|
|
1400
1517
|
},
|
|
1518
|
+
onSave: (body) => finishCommentEdit(jobId, c.id, body),
|
|
1519
|
+
onCancel: () => finishCommentEdit(jobId, c.id, null),
|
|
1401
1520
|
})
|
|
1402
1521
|
);
|
|
1403
1522
|
continue;
|
|
@@ -1415,9 +1534,7 @@ function renderDiff(diff, opts = {}) {
|
|
|
1415
1534
|
edit.className = 'btn btn-muted-text btn-sm';
|
|
1416
1535
|
edit.textContent = 'Edit';
|
|
1417
1536
|
edit.addEventListener('click', () => {
|
|
1418
|
-
|
|
1419
|
-
activeInlineCommentKey = null;
|
|
1420
|
-
refreshReviewCommentUi(jobId);
|
|
1537
|
+
startCommentEdit(c.id, 'inline', jobId);
|
|
1421
1538
|
});
|
|
1422
1539
|
const remove = document.createElement('button');
|
|
1423
1540
|
remove.type = 'button';
|
|
@@ -1427,7 +1544,7 @@ function renderDiff(diff, opts = {}) {
|
|
|
1427
1544
|
const d = loadReviewDraft(jobId);
|
|
1428
1545
|
d.lineComments = d.lineComments.filter((x) => x.id !== c.id);
|
|
1429
1546
|
saveReviewDraft(jobId);
|
|
1430
|
-
if (editingCommentId === c.id)
|
|
1547
|
+
if (editingCommentId === c.id) clearCommentEditState();
|
|
1431
1548
|
refreshReviewCommentUi(jobId);
|
|
1432
1549
|
});
|
|
1433
1550
|
actions.appendChild(edit);
|
|
@@ -1684,22 +1801,16 @@ function renderPendingCommentList(jobId) {
|
|
|
1684
1801
|
loc.textContent = `${c.path}:${c.line} (${c.side})`;
|
|
1685
1802
|
main.appendChild(loc);
|
|
1686
1803
|
|
|
1687
|
-
if (editingCommentId === c.id) {
|
|
1804
|
+
if (editingCommentId === c.id && editingCommentSurface === 'pending') {
|
|
1688
1805
|
main.appendChild(
|
|
1689
1806
|
buildCommentEditForm({
|
|
1690
1807
|
initialBody: c.body,
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
if (target) target.body = body;
|
|
1695
|
-
saveReviewDraft(jobId);
|
|
1696
|
-
editingCommentId = null;
|
|
1697
|
-
refreshReviewCommentUi(jobId);
|
|
1698
|
-
},
|
|
1699
|
-
onCancel: () => {
|
|
1700
|
-
editingCommentId = null;
|
|
1701
|
-
refreshReviewCommentUi(jobId);
|
|
1808
|
+
onInput: (body) => {
|
|
1809
|
+
updateLineCommentBody(jobId, c.id, body);
|
|
1810
|
+
mirrorCommentEditToOtherSurface(jobId);
|
|
1702
1811
|
},
|
|
1812
|
+
onSave: (body) => finishCommentEdit(jobId, c.id, body),
|
|
1813
|
+
onCancel: () => finishCommentEdit(jobId, c.id, null),
|
|
1703
1814
|
})
|
|
1704
1815
|
);
|
|
1705
1816
|
item.appendChild(main);
|
|
@@ -1718,9 +1829,7 @@ function renderPendingCommentList(jobId) {
|
|
|
1718
1829
|
edit.className = 'btn btn-muted-text btn-sm';
|
|
1719
1830
|
edit.textContent = 'Edit';
|
|
1720
1831
|
edit.addEventListener('click', () => {
|
|
1721
|
-
|
|
1722
|
-
activeInlineCommentKey = null;
|
|
1723
|
-
refreshReviewCommentUi(jobId);
|
|
1832
|
+
startCommentEdit(c.id, 'pending', jobId);
|
|
1724
1833
|
});
|
|
1725
1834
|
const remove = document.createElement('button');
|
|
1726
1835
|
remove.type = 'button';
|
|
@@ -1730,7 +1839,7 @@ function renderPendingCommentList(jobId) {
|
|
|
1730
1839
|
const d = loadReviewDraft(jobId);
|
|
1731
1840
|
d.lineComments = d.lineComments.filter((x) => x.id !== c.id);
|
|
1732
1841
|
saveReviewDraft(jobId);
|
|
1733
|
-
if (editingCommentId === c.id)
|
|
1842
|
+
if (editingCommentId === c.id) clearCommentEditState();
|
|
1734
1843
|
refreshReviewCommentUi(jobId);
|
|
1735
1844
|
});
|
|
1736
1845
|
actions.appendChild(edit);
|
|
@@ -2321,7 +2430,7 @@ function renderReview(jobs) {
|
|
|
2321
2430
|
const focusInPending = Boolean(
|
|
2322
2431
|
els.reviewPendingList?.contains(document.activeElement)
|
|
2323
2432
|
);
|
|
2324
|
-
if (!(editingCommentId && focusInPending)) {
|
|
2433
|
+
if (!(editingCommentId && editingCommentSurface === 'pending' && focusInPending)) {
|
|
2325
2434
|
renderPendingCommentList(job.id);
|
|
2326
2435
|
}
|
|
2327
2436
|
syncSubmitReviewButton(job.id);
|
|
@@ -2353,7 +2462,9 @@ function renderReview(jobs) {
|
|
|
2353
2462
|
|
|
2354
2463
|
const focusInDiff = els.diffViewer?.contains(document.activeElement);
|
|
2355
2464
|
const preservingDiffCompose =
|
|
2356
|
-
(activeInlineCommentKey ||
|
|
2465
|
+
(Boolean(activeInlineCommentKey) ||
|
|
2466
|
+
(editingCommentId && editingCommentSurface === 'inline')) &&
|
|
2467
|
+
focusInDiff;
|
|
2357
2468
|
if (!preservingDiffCompose) {
|
|
2358
2469
|
renderDiff(displayDiff, { interactive: editable, jobId: job.id });
|
|
2359
2470
|
}
|
|
@@ -2813,13 +2924,94 @@ async function readJson(res) {
|
|
|
2813
2924
|
}
|
|
2814
2925
|
}
|
|
2815
2926
|
|
|
2816
|
-
function
|
|
2817
|
-
els.
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2927
|
+
function updateModelLabel() {
|
|
2928
|
+
if (!els.modelCurrent) return;
|
|
2929
|
+
const id = currentModel || '—';
|
|
2930
|
+
els.modelCurrent.textContent = id;
|
|
2931
|
+
els.modelCurrent.title = `Model: ${id} — change in Settings → Configuration`;
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
/**
|
|
2935
|
+
* @param {'anthropic' | 'fallback' | string | undefined} source
|
|
2936
|
+
*/
|
|
2937
|
+
function updateModelSourceHint(source) {
|
|
2938
|
+
if (!els.settingsModelHint) return;
|
|
2939
|
+
if (source === 'anthropic') {
|
|
2940
|
+
els.settingsModelHint.textContent =
|
|
2941
|
+
'Claude model for agent runs. List loaded from Anthropic Models API.';
|
|
2942
|
+
} else if (source === 'fallback') {
|
|
2943
|
+
els.settingsModelHint.textContent =
|
|
2944
|
+
'Claude model for agent runs. Showing curated Claude Code models (live list unavailable — set an API key or use claude auth login).';
|
|
2945
|
+
} else {
|
|
2946
|
+
els.settingsModelHint.textContent = 'Claude model for agent runs.';
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
/**
|
|
2951
|
+
* @param {{ models?: Array<{id:string,label?:string,name?:string}>, selected?: string, source?: string }} data
|
|
2952
|
+
*/
|
|
2953
|
+
function populateSettingsModels(data) {
|
|
2954
|
+
const models = Array.isArray(data?.models) && data.models.length
|
|
2955
|
+
? data.models
|
|
2956
|
+
: availableModels.length
|
|
2957
|
+
? availableModels
|
|
2958
|
+
: [
|
|
2959
|
+
{ id: 'claude-sonnet-5', label: 'Sonnet 5' },
|
|
2960
|
+
{ id: 'claude-opus-5', label: 'Opus 5' },
|
|
2961
|
+
{ id: 'claude-fable-5', label: 'Fable 5' },
|
|
2962
|
+
{ id: 'claude-haiku-4-5', label: 'Haiku 4.5' },
|
|
2963
|
+
];
|
|
2964
|
+
availableModels = models;
|
|
2965
|
+
if (data?.selected) currentModel = data.selected;
|
|
2966
|
+
if (data?.source) updateModelSourceHint(data.source);
|
|
2967
|
+
|
|
2968
|
+
if (els.settingsModel) {
|
|
2969
|
+
const currentIds = [...els.settingsModel.options].map((o) => o.value);
|
|
2970
|
+
const nextIds = models.map((m) => m.id);
|
|
2971
|
+
if (currentIds.join(',') !== nextIds.join(',')) {
|
|
2972
|
+
els.settingsModel.innerHTML = '';
|
|
2973
|
+
for (const m of models) {
|
|
2974
|
+
const opt = document.createElement('option');
|
|
2975
|
+
opt.value = m.id;
|
|
2976
|
+
opt.textContent = m.label || m.name || m.id;
|
|
2977
|
+
els.settingsModel.appendChild(opt);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
const selected = currentModel;
|
|
2981
|
+
if (selected && [...els.settingsModel.options].some((o) => o.value === selected)) {
|
|
2982
|
+
els.settingsModel.value = selected;
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
updateModelLabel();
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
async function fetchModels(opts = {}) {
|
|
2989
|
+
const refresh = opts.refresh === true;
|
|
2990
|
+
try {
|
|
2991
|
+
const res = await fetch(refresh ? '/api/models?refresh=1' : '/api/models');
|
|
2992
|
+
if (!res.ok) {
|
|
2993
|
+
populateSettingsModels({
|
|
2994
|
+
models: availableModels.length
|
|
2995
|
+
? availableModels
|
|
2996
|
+
: [
|
|
2997
|
+
{ id: currentModel || 'claude-sonnet-5', label: currentModel || 'claude-sonnet-5' },
|
|
2998
|
+
],
|
|
2999
|
+
selected: currentModel,
|
|
3000
|
+
});
|
|
3001
|
+
return;
|
|
3002
|
+
}
|
|
3003
|
+
const data = await readJson(res);
|
|
3004
|
+
populateSettingsModels(data);
|
|
3005
|
+
} catch (err) {
|
|
3006
|
+
console.error('Failed to fetch models:', err);
|
|
3007
|
+
populateSettingsModels({
|
|
3008
|
+
models: availableModels.length
|
|
3009
|
+
? availableModels
|
|
3010
|
+
: [
|
|
3011
|
+
{ id: currentModel || 'claude-sonnet-5', label: currentModel || 'claude-sonnet-5' },
|
|
3012
|
+
],
|
|
3013
|
+
selected: currentModel,
|
|
3014
|
+
});
|
|
2823
3015
|
}
|
|
2824
3016
|
}
|
|
2825
3017
|
|
|
@@ -2847,9 +3039,160 @@ function knownToolsList(cfg = appConfig) {
|
|
|
2847
3039
|
: DEFAULT_KNOWN_TOOLS;
|
|
2848
3040
|
}
|
|
2849
3041
|
|
|
3042
|
+
/**
|
|
3043
|
+
* Switch Settings top-level tab (Ticket source / Authentication / Rules / Configuration).
|
|
3044
|
+
* @param {'ticket' | 'auth' | 'rules' | 'config'} tab
|
|
3045
|
+
*/
|
|
3046
|
+
function setSettingsTab(tab) {
|
|
3047
|
+
const next =
|
|
3048
|
+
tab === 'auth' || tab === 'rules' || tab === 'config' ? tab : 'ticket';
|
|
3049
|
+
settingsTab = next;
|
|
3050
|
+
|
|
3051
|
+
if (els.settingsTabs) {
|
|
3052
|
+
els.settingsTabs.querySelectorAll('[role="tab"]').forEach((btn) => {
|
|
3053
|
+
const selected = btn.dataset.settingsTab === next;
|
|
3054
|
+
btn.setAttribute('aria-selected', selected ? 'true' : 'false');
|
|
3055
|
+
btn.tabIndex = selected ? 0 : -1;
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
document.querySelectorAll('[data-settings-panel]').forEach((panel) => {
|
|
3060
|
+
const show = panel.dataset.settingsPanel === next;
|
|
3061
|
+
panel.classList.toggle('hidden', !show);
|
|
3062
|
+
});
|
|
3063
|
+
|
|
3064
|
+
if (next === 'config') {
|
|
3065
|
+
void fetchModels();
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
/**
|
|
3070
|
+
* Empty = keep existing secret; data-clear + empty = send null to clear.
|
|
3071
|
+
* @param {HTMLInputElement | null} inputEl
|
|
3072
|
+
* @returns {string | null | undefined}
|
|
3073
|
+
*/
|
|
3074
|
+
function secretPatchValue(inputEl) {
|
|
3075
|
+
const typed = inputEl?.value?.trim() || '';
|
|
3076
|
+
if (typed) return typed;
|
|
3077
|
+
if (inputEl?.dataset.clear === '1') return null;
|
|
3078
|
+
return undefined;
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
/**
|
|
3082
|
+
* @param {HTMLInputElement | null} inputEl
|
|
3083
|
+
* @param {HTMLButtonElement | null} clearBtn
|
|
3084
|
+
* @param {boolean} set
|
|
3085
|
+
* @param {string | null | undefined} masked
|
|
3086
|
+
* @param {string} unsetPlaceholder
|
|
3087
|
+
*/
|
|
3088
|
+
function fillSecretInput(inputEl, clearBtn, set, masked, unsetPlaceholder) {
|
|
3089
|
+
if (!inputEl) return;
|
|
3090
|
+
inputEl.value = '';
|
|
3091
|
+
delete inputEl.dataset.clear;
|
|
3092
|
+
inputEl.placeholder = set
|
|
3093
|
+
? `Saved (${masked || '••••'}) — leave blank to keep`
|
|
3094
|
+
: unsetPlaceholder;
|
|
3095
|
+
if (clearBtn) {
|
|
3096
|
+
clearBtn.classList.toggle('hidden', !set);
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
function ghStatusText(cfg) {
|
|
3101
|
+
if (!cfg.ghInstalled) {
|
|
3102
|
+
return 'gh is not installed (required for issues and PRs)';
|
|
3103
|
+
}
|
|
3104
|
+
if (cfg.ghAuthOk) {
|
|
3105
|
+
const method =
|
|
3106
|
+
cfg.ghAuthMethod === 'token'
|
|
3107
|
+
? 'env token (GH_TOKEN)'
|
|
3108
|
+
: 'gh auth login';
|
|
3109
|
+
return `Authenticated via ${method}`;
|
|
3110
|
+
}
|
|
3111
|
+
if (cfg.ghTokenSet) {
|
|
3112
|
+
return 'Token saved, but gh is not authenticated — check the token';
|
|
3113
|
+
}
|
|
3114
|
+
return 'Not authenticated — paste a PAT or run gh auth login';
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
function claudeStatusText(cfg) {
|
|
3118
|
+
switch (cfg.claudeAuthMethod) {
|
|
3119
|
+
case 'api-key':
|
|
3120
|
+
return 'Authenticated via ANTHROPIC_API_KEY (API billing)';
|
|
3121
|
+
case 'auth-token':
|
|
3122
|
+
return 'Authenticated via ANTHROPIC_AUTH_TOKEN';
|
|
3123
|
+
case 'oauth-token':
|
|
3124
|
+
return 'Authenticated via CLAUDE_CODE_OAUTH_TOKEN';
|
|
3125
|
+
case 'claude-code-login':
|
|
3126
|
+
return 'Authenticated via claude auth login (subscription)';
|
|
3127
|
+
default:
|
|
3128
|
+
return 'Not authenticated — add an API key, OAuth token, or run claude auth login';
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
function fillAuthSettings(cfg) {
|
|
3133
|
+
if (els.settingsGhStatus) {
|
|
3134
|
+
els.settingsGhStatus.textContent = ghStatusText(cfg);
|
|
3135
|
+
els.settingsGhStatus.className =
|
|
3136
|
+
cfg.ghAuthOk ? 'auth-status ok' : 'auth-status err';
|
|
3137
|
+
}
|
|
3138
|
+
if (els.settingsGhOrigin) {
|
|
3139
|
+
if (cfg.originRemoteUrl) {
|
|
3140
|
+
els.settingsGhOrigin.textContent = cfg.originRemoteSsh
|
|
3141
|
+
? `origin: ${cfg.originRemoteUrl} (SSH — git push/fetch only, not gh API auth)`
|
|
3142
|
+
: `origin: ${cfg.originRemoteUrl}`;
|
|
3143
|
+
} else {
|
|
3144
|
+
els.settingsGhOrigin.textContent = '';
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
fillSecretInput(
|
|
3148
|
+
els.settingsGhToken,
|
|
3149
|
+
els.settingsGhTokenClear,
|
|
3150
|
+
Boolean(cfg.ghTokenSet),
|
|
3151
|
+
cfg.ghTokenMasked,
|
|
3152
|
+
'Paste a GitHub PAT'
|
|
3153
|
+
);
|
|
3154
|
+
if (els.settingsGhTokenHint) {
|
|
3155
|
+
els.settingsGhTokenHint.innerHTML = cfg.ghTokenSet
|
|
3156
|
+
? 'Token stored in <code>.acdev/.env</code> as <code>GH_TOKEN</code> (not committed). Leave blank to keep. SSH remotes can push/fetch git but do <strong>not</strong> authenticate <code>gh</code> for issues or PRs.'
|
|
3157
|
+
: 'If <code>gh auth login</code> already works on this machine, leave this blank. Otherwise paste a PAT (<code>repo</code> and pull-request scopes). Stored as <code>GH_TOKEN</code> so <code>gh</code> can auth without a browser. SSH remotes can push/fetch git but do <strong>not</strong> authenticate <code>gh</code> for issues or PRs.';
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
if (els.settingsClaudeStatus) {
|
|
3161
|
+
els.settingsClaudeStatus.textContent = claudeStatusText(cfg);
|
|
3162
|
+
els.settingsClaudeStatus.className =
|
|
3163
|
+
cfg.claudeAuthOk ? 'auth-status ok' : 'auth-status err';
|
|
3164
|
+
}
|
|
3165
|
+
fillSecretInput(
|
|
3166
|
+
els.settingsAnthropicKey,
|
|
3167
|
+
els.settingsAnthropicKeyClear,
|
|
3168
|
+
Boolean(cfg.anthropicApiKeySet),
|
|
3169
|
+
cfg.anthropicApiKeyMasked,
|
|
3170
|
+
'Paste an Anthropic API key'
|
|
3171
|
+
);
|
|
3172
|
+
fillSecretInput(
|
|
3173
|
+
els.settingsClaudeOauth,
|
|
3174
|
+
els.settingsClaudeOauthClear,
|
|
3175
|
+
Boolean(cfg.claudeOauthTokenSet),
|
|
3176
|
+
cfg.claudeOauthTokenMasked,
|
|
3177
|
+
'Paste token from claude setup-token'
|
|
3178
|
+
);
|
|
3179
|
+
if (els.settingsAnthropicKeyHint) {
|
|
3180
|
+
els.settingsAnthropicKeyHint.innerHTML = cfg.anthropicApiKeySet
|
|
3181
|
+
? 'API key stored in <code>.acdev/.env</code> (not committed). Takes precedence over subscription login — clear it to use <code>claude auth login</code>.'
|
|
3182
|
+
: 'Console / pay-as-you-go billing. Takes precedence over subscription login. Stored as <code>ANTHROPIC_API_KEY</code>. Clear or omit the key to use <code>claude auth login</code> instead.';
|
|
3183
|
+
}
|
|
3184
|
+
if (els.settingsClaudeOauthHint) {
|
|
3185
|
+
els.settingsClaudeOauthHint.innerHTML = cfg.claudeOauthTokenSet
|
|
3186
|
+
? '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.'
|
|
3187
|
+
: '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.';
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
|
|
2850
3191
|
function fillSettingsForm(cfg) {
|
|
2851
3192
|
if (!els.settingsForm || !cfg) return;
|
|
2852
3193
|
|
|
3194
|
+
fillAuthSettings(cfg);
|
|
3195
|
+
|
|
2853
3196
|
updateTicketSourceUI(cfg.ticketSource === 'jira' ? 'jira' : 'github');
|
|
2854
3197
|
|
|
2855
3198
|
if (els.settingsJiraBaseUrl) {
|
|
@@ -2877,24 +3220,37 @@ function fillSettingsForm(cfg) {
|
|
|
2877
3220
|
if (els.settingsJiraRuleEnabled) {
|
|
2878
3221
|
els.settingsJiraRuleEnabled.checked = Boolean(jiraRule.enabled);
|
|
2879
3222
|
}
|
|
3223
|
+
if (els.settingsJiraRuleAction) {
|
|
3224
|
+
const action = ['none', 'set_status', 'add_label', 'close_issue'].includes(jiraRule.action)
|
|
3225
|
+
? jiraRule.action
|
|
3226
|
+
: 'none';
|
|
3227
|
+
els.settingsJiraRuleAction.value = action;
|
|
3228
|
+
}
|
|
2880
3229
|
if (els.settingsJiraRuleStatus) {
|
|
2881
3230
|
els.settingsJiraRuleStatus.value = jiraRule.targetStatus || 'In Review';
|
|
2882
3231
|
}
|
|
3232
|
+
if (els.settingsJiraRuleLabel) {
|
|
3233
|
+
els.settingsJiraRuleLabel.value = jiraRule.label || '';
|
|
3234
|
+
}
|
|
2883
3235
|
|
|
2884
3236
|
const ghRule = cfg.githubRules?.afterPrOpened || {};
|
|
2885
3237
|
if (els.settingsGithubRuleEnabled) {
|
|
2886
3238
|
els.settingsGithubRuleEnabled.checked = Boolean(ghRule.enabled);
|
|
2887
3239
|
}
|
|
2888
3240
|
if (els.settingsGithubRuleAction) {
|
|
2889
|
-
const action = ['none', 'add_label', 'close_issue'].includes(ghRule.action)
|
|
3241
|
+
const action = ['none', 'set_status', 'add_label', 'close_issue'].includes(ghRule.action)
|
|
2890
3242
|
? ghRule.action
|
|
2891
3243
|
: 'none';
|
|
2892
3244
|
els.settingsGithubRuleAction.value = action;
|
|
2893
3245
|
}
|
|
3246
|
+
if (els.settingsGithubRuleStatus) {
|
|
3247
|
+
els.settingsGithubRuleStatus.value = ghRule.targetStatus || 'In Review';
|
|
3248
|
+
}
|
|
2894
3249
|
if (els.settingsGithubRuleLabel) {
|
|
2895
3250
|
els.settingsGithubRuleLabel.value = ghRule.label || '';
|
|
2896
3251
|
}
|
|
2897
|
-
|
|
3252
|
+
updateJiraRuleFieldsVisibility();
|
|
3253
|
+
updateGithubRuleFieldsVisibility();
|
|
2898
3254
|
|
|
2899
3255
|
if (els.jiraStatus) {
|
|
2900
3256
|
els.jiraStatus.textContent = cfg.jiraConfigured
|
|
@@ -2910,19 +3266,15 @@ function fillSettingsForm(cfg) {
|
|
|
2910
3266
|
}
|
|
2911
3267
|
|
|
2912
3268
|
if (els.settingsModel) {
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
opt.value = m.id;
|
|
2921
|
-
opt.textContent = m.label || m.id;
|
|
2922
|
-
els.settingsModel.appendChild(opt);
|
|
2923
|
-
}
|
|
3269
|
+
if (availableModels.length) {
|
|
3270
|
+
populateSettingsModels({ models: availableModels, selected: cfg.model || currentModel });
|
|
3271
|
+
} else if (cfg.model) {
|
|
3272
|
+
populateSettingsModels({
|
|
3273
|
+
models: [{ id: cfg.model, label: cfg.model }],
|
|
3274
|
+
selected: cfg.model,
|
|
3275
|
+
});
|
|
2924
3276
|
}
|
|
2925
|
-
|
|
3277
|
+
void fetchModels();
|
|
2926
3278
|
}
|
|
2927
3279
|
|
|
2928
3280
|
if (els.settingsMaxTurns) {
|
|
@@ -2964,12 +3316,39 @@ function fillSettingsForm(cfg) {
|
|
|
2964
3316
|
}
|
|
2965
3317
|
|
|
2966
3318
|
/**
|
|
2967
|
-
* Show
|
|
3319
|
+
* Show status / label fields based on the selected post-PR action.
|
|
3320
|
+
* @param {'jira' | 'github'} source
|
|
3321
|
+
*/
|
|
3322
|
+
function updateRuleFieldsVisibility(source) {
|
|
3323
|
+
const actionEl =
|
|
3324
|
+
source === 'jira' ? els.settingsJiraRuleAction : els.settingsGithubRuleAction;
|
|
3325
|
+
const statusField =
|
|
3326
|
+
source === 'jira'
|
|
3327
|
+
? els.settingsJiraRuleStatusField
|
|
3328
|
+
: els.settingsGithubRuleStatusField;
|
|
3329
|
+
const labelField =
|
|
3330
|
+
source === 'jira'
|
|
3331
|
+
? els.settingsJiraRuleLabelField
|
|
3332
|
+
: els.settingsGithubRuleLabelField;
|
|
3333
|
+
if (!actionEl) return;
|
|
3334
|
+
const action = actionEl.value;
|
|
3335
|
+
if (statusField) {
|
|
3336
|
+
statusField.classList.toggle('hidden', action !== 'set_status');
|
|
3337
|
+
}
|
|
3338
|
+
if (labelField) {
|
|
3339
|
+
labelField.classList.toggle('hidden', action !== 'add_label');
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
function updateJiraRuleFieldsVisibility() {
|
|
3344
|
+
updateRuleFieldsVisibility('jira');
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
/**
|
|
3348
|
+
* Show status / label fields based on the selected GitHub post-PR action.
|
|
2968
3349
|
*/
|
|
2969
|
-
function
|
|
2970
|
-
|
|
2971
|
-
const show = els.settingsGithubRuleAction.value === 'add_label';
|
|
2972
|
-
els.settingsGithubRuleLabelField.classList.toggle('hidden', !show);
|
|
3350
|
+
function updateGithubRuleFieldsVisibility() {
|
|
3351
|
+
updateRuleFieldsVisibility('github');
|
|
2973
3352
|
}
|
|
2974
3353
|
|
|
2975
3354
|
/**
|
|
@@ -2989,12 +3368,8 @@ function updateTicketSourceUI(source) {
|
|
|
2989
3368
|
els.jiraConnectPanel.classList.toggle('hidden', ticketSource !== 'jira');
|
|
2990
3369
|
}
|
|
2991
3370
|
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
}
|
|
2995
|
-
if (els.githubRulesPanel) {
|
|
2996
|
-
els.githubRulesPanel.classList.toggle('hidden', ticketSource !== 'github');
|
|
2997
|
-
}
|
|
3371
|
+
// Rules tab always shows both GitHub and Jira post-PR options
|
|
3372
|
+
// (ticket source only gates the Jira connect panel above).
|
|
2998
3373
|
|
|
2999
3374
|
if (els.ticketSourceMeta) {
|
|
3000
3375
|
els.ticketSourceMeta.textContent =
|
|
@@ -3044,7 +3419,7 @@ function updateTimeoutMsHint(minutes) {
|
|
|
3044
3419
|
function applyConfigSnapshot(data) {
|
|
3045
3420
|
appConfig = data || {};
|
|
3046
3421
|
if (data?.model) currentModel = data.model;
|
|
3047
|
-
|
|
3422
|
+
updateModelLabel();
|
|
3048
3423
|
updateTicketSourceUI(data?.ticketSource === 'jira' ? 'jira' : 'github');
|
|
3049
3424
|
if (els.repoName) {
|
|
3050
3425
|
els.repoName.textContent = data?.repoName || 'local repo';
|
|
@@ -3068,35 +3443,6 @@ async function fetchConfig() {
|
|
|
3068
3443
|
}
|
|
3069
3444
|
}
|
|
3070
3445
|
|
|
3071
|
-
async function setModel(model) {
|
|
3072
|
-
if (modelSaving || model === currentModel) return;
|
|
3073
|
-
modelSaving = true;
|
|
3074
|
-
const prev = currentModel;
|
|
3075
|
-
currentModel = model;
|
|
3076
|
-
updateModelButtons();
|
|
3077
|
-
try {
|
|
3078
|
-
const res = await fetch('/api/config', {
|
|
3079
|
-
method: 'PATCH',
|
|
3080
|
-
headers: { 'Content-Type': 'application/json' },
|
|
3081
|
-
body: JSON.stringify({ model }),
|
|
3082
|
-
});
|
|
3083
|
-
const data = await readJson(res);
|
|
3084
|
-
if (!res.ok) {
|
|
3085
|
-
currentModel = prev;
|
|
3086
|
-
updateModelButtons();
|
|
3087
|
-
alert(`Model update failed: ${data.error || `HTTP ${res.status}`}`);
|
|
3088
|
-
return;
|
|
3089
|
-
}
|
|
3090
|
-
applyConfigSnapshot({ ...appConfig, ...data });
|
|
3091
|
-
} catch (err) {
|
|
3092
|
-
currentModel = prev;
|
|
3093
|
-
updateModelButtons();
|
|
3094
|
-
alert(`Model update failed: ${err.message}`);
|
|
3095
|
-
} finally {
|
|
3096
|
-
modelSaving = false;
|
|
3097
|
-
}
|
|
3098
|
-
}
|
|
3099
|
-
|
|
3100
3446
|
function readSettingsForm() {
|
|
3101
3447
|
const baseBranch = els.settingsBaseBranch?.value?.trim() || '';
|
|
3102
3448
|
const model = els.settingsModel?.value || currentModel;
|
|
@@ -3120,6 +3466,13 @@ function readSettingsForm() {
|
|
|
3120
3466
|
ticketSource,
|
|
3121
3467
|
};
|
|
3122
3468
|
|
|
3469
|
+
const ghToken = secretPatchValue(els.settingsGhToken);
|
|
3470
|
+
if (ghToken !== undefined) patch.ghToken = ghToken;
|
|
3471
|
+
const anthropicApiKey = secretPatchValue(els.settingsAnthropicKey);
|
|
3472
|
+
if (anthropicApiKey !== undefined) patch.anthropicApiKey = anthropicApiKey;
|
|
3473
|
+
const claudeOauthToken = secretPatchValue(els.settingsClaudeOauth);
|
|
3474
|
+
if (claudeOauthToken !== undefined) patch.claudeOauthToken = claudeOauthToken;
|
|
3475
|
+
|
|
3123
3476
|
if (ticketSource === 'jira' || els.settingsJiraBaseUrl?.value) {
|
|
3124
3477
|
patch.jiraBaseUrl = els.settingsJiraBaseUrl?.value?.trim() || '';
|
|
3125
3478
|
patch.jiraPrLinkPhrase =
|
|
@@ -3130,28 +3483,31 @@ function readSettingsForm() {
|
|
|
3130
3483
|
if (token) patch.jiraApiToken = token;
|
|
3131
3484
|
}
|
|
3132
3485
|
|
|
3133
|
-
//
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3486
|
+
// Both rule panels are always visible on the Rules tab — PATCH both.
|
|
3487
|
+
const jiraAction = els.settingsJiraRuleAction?.value || 'none';
|
|
3488
|
+
patch.jiraRules = {
|
|
3489
|
+
afterPrOpened: {
|
|
3490
|
+
enabled: Boolean(els.settingsJiraRuleEnabled?.checked),
|
|
3491
|
+
action: ['none', 'set_status', 'add_label', 'close_issue'].includes(jiraAction)
|
|
3492
|
+
? jiraAction
|
|
3493
|
+
: 'none',
|
|
3494
|
+
targetStatus:
|
|
3495
|
+
els.settingsJiraRuleStatus?.value?.trim() || 'In Review',
|
|
3496
|
+
label: els.settingsJiraRuleLabel?.value?.trim() || '',
|
|
3497
|
+
},
|
|
3498
|
+
};
|
|
3499
|
+
const ghAction = els.settingsGithubRuleAction?.value || 'none';
|
|
3500
|
+
patch.githubRules = {
|
|
3501
|
+
afterPrOpened: {
|
|
3502
|
+
enabled: Boolean(els.settingsGithubRuleEnabled?.checked),
|
|
3503
|
+
action: ['none', 'set_status', 'add_label', 'close_issue'].includes(ghAction)
|
|
3504
|
+
? ghAction
|
|
3505
|
+
: 'none',
|
|
3506
|
+
targetStatus:
|
|
3507
|
+
els.settingsGithubRuleStatus?.value?.trim() || 'In Review',
|
|
3508
|
+
label: els.settingsGithubRuleLabel?.value?.trim() || '',
|
|
3509
|
+
},
|
|
3510
|
+
};
|
|
3155
3511
|
|
|
3156
3512
|
return patch;
|
|
3157
3513
|
}
|
|
@@ -3190,6 +3546,7 @@ async function saveSettings(event) {
|
|
|
3190
3546
|
}
|
|
3191
3547
|
applyConfigSnapshot(data);
|
|
3192
3548
|
fillSettingsForm(data);
|
|
3549
|
+
void fetchModels({ refresh: true });
|
|
3193
3550
|
setSettingsFeedback('Settings saved. Next jobs will use these values.', 'ok');
|
|
3194
3551
|
} catch (err) {
|
|
3195
3552
|
setSettingsFeedback(err.message || 'Save failed', 'error');
|
|
@@ -3209,15 +3566,79 @@ els.themeToggle.addEventListener('click', () => {
|
|
|
3209
3566
|
applyTheme(next);
|
|
3210
3567
|
});
|
|
3211
3568
|
|
|
3212
|
-
els.modelToggle.querySelectorAll('.model-btn').forEach((btn) => {
|
|
3213
|
-
btn.addEventListener('click', () => setModel(btn.dataset.model));
|
|
3214
|
-
});
|
|
3215
|
-
|
|
3216
3569
|
els.settingsForm?.addEventListener('submit', saveSettings);
|
|
3217
3570
|
els.settingsTimeout?.addEventListener('input', () => {
|
|
3218
3571
|
updateTimeoutMsHint(els.settingsTimeout.value);
|
|
3219
3572
|
});
|
|
3220
|
-
els.
|
|
3573
|
+
els.settingsJiraRuleAction?.addEventListener('change', updateJiraRuleFieldsVisibility);
|
|
3574
|
+
els.settingsGithubRuleAction?.addEventListener('change', updateGithubRuleFieldsVisibility);
|
|
3575
|
+
|
|
3576
|
+
els.settingsTabs?.addEventListener('click', (e) => {
|
|
3577
|
+
const btn = e.target.closest('[data-settings-tab]');
|
|
3578
|
+
if (!btn?.dataset.settingsTab) return;
|
|
3579
|
+
setSettingsTab(
|
|
3580
|
+
/** @type {'ticket' | 'auth' | 'rules' | 'config'} */ (btn.dataset.settingsTab)
|
|
3581
|
+
);
|
|
3582
|
+
});
|
|
3583
|
+
|
|
3584
|
+
els.settingsTabs?.addEventListener('keydown', (e) => {
|
|
3585
|
+
const tabs = [...(els.settingsTabs?.querySelectorAll('[role="tab"]') || [])];
|
|
3586
|
+
if (!tabs.length) return;
|
|
3587
|
+
const current = document.activeElement;
|
|
3588
|
+
const idx = tabs.indexOf(/** @type {HTMLElement} */ (current));
|
|
3589
|
+
if (idx < 0) return;
|
|
3590
|
+
let nextIdx = -1;
|
|
3591
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
|
3592
|
+
nextIdx = (idx + 1) % tabs.length;
|
|
3593
|
+
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
|
3594
|
+
nextIdx = (idx - 1 + tabs.length) % tabs.length;
|
|
3595
|
+
} else if (e.key === 'Home') {
|
|
3596
|
+
nextIdx = 0;
|
|
3597
|
+
} else if (e.key === 'End') {
|
|
3598
|
+
nextIdx = tabs.length - 1;
|
|
3599
|
+
}
|
|
3600
|
+
if (nextIdx < 0) return;
|
|
3601
|
+
e.preventDefault();
|
|
3602
|
+
const nextTab = tabs[nextIdx];
|
|
3603
|
+
setSettingsTab(
|
|
3604
|
+
/** @type {'ticket' | 'auth' | 'rules' | 'config'} */ (nextTab.dataset.settingsTab)
|
|
3605
|
+
);
|
|
3606
|
+
nextTab.focus();
|
|
3607
|
+
});
|
|
3608
|
+
|
|
3609
|
+
/**
|
|
3610
|
+
* @param {HTMLInputElement | null} inputEl
|
|
3611
|
+
* @param {HTMLElement | null} hintEl
|
|
3612
|
+
* @param {string} clearedHint
|
|
3613
|
+
*/
|
|
3614
|
+
function bindSecretClear(clearBtn, inputEl, hintEl, clearedHint) {
|
|
3615
|
+
clearBtn?.addEventListener('click', () => {
|
|
3616
|
+
if (!inputEl) return;
|
|
3617
|
+
inputEl.value = '';
|
|
3618
|
+
inputEl.dataset.clear = '1';
|
|
3619
|
+
inputEl.placeholder = 'Will be removed on save';
|
|
3620
|
+
if (hintEl) hintEl.textContent = clearedHint;
|
|
3621
|
+
});
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
bindSecretClear(
|
|
3625
|
+
els.settingsGhTokenClear,
|
|
3626
|
+
els.settingsGhToken,
|
|
3627
|
+
els.settingsGhTokenHint,
|
|
3628
|
+
'Saved GH_TOKEN will be removed when you click Save settings.'
|
|
3629
|
+
);
|
|
3630
|
+
bindSecretClear(
|
|
3631
|
+
els.settingsAnthropicKeyClear,
|
|
3632
|
+
els.settingsAnthropicKey,
|
|
3633
|
+
els.settingsAnthropicKeyHint,
|
|
3634
|
+
'Saved API key will be removed on save so subscription login can be used.'
|
|
3635
|
+
);
|
|
3636
|
+
bindSecretClear(
|
|
3637
|
+
els.settingsClaudeOauthClear,
|
|
3638
|
+
els.settingsClaudeOauth,
|
|
3639
|
+
els.settingsClaudeOauthHint,
|
|
3640
|
+
'Saved CLAUDE_CODE_OAUTH_TOKEN will be removed when you click Save settings.'
|
|
3641
|
+
);
|
|
3221
3642
|
|
|
3222
3643
|
els.addBtn.addEventListener('click', async () => {
|
|
3223
3644
|
els.enqueueFeedback.classList.add('hidden');
|
|
@@ -3538,5 +3959,6 @@ els.logJobFilter.addEventListener('change', () => {
|
|
|
3538
3959
|
|
|
3539
3960
|
applyTheme(loadTheme());
|
|
3540
3961
|
fetchConfig();
|
|
3962
|
+
fetchModels();
|
|
3541
3963
|
fetchJobs();
|
|
3542
3964
|
setInterval(fetchJobs, 3000);
|