acdev 1.0.2 → 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 +438 -93
- 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
|
@@ -124,10 +124,13 @@ let reviewDrafts = {};
|
|
|
124
124
|
let reviewFileLists = {};
|
|
125
125
|
/** @type {Record<string, Set<string>>} excluded paths per job while reviewing */
|
|
126
126
|
let reviewExcludedByJob = {};
|
|
127
|
-
let modelSaving = false;
|
|
128
127
|
let settingsSaving = false;
|
|
128
|
+
/** @type {'ticket' | 'auth' | 'rules' | 'config'} */
|
|
129
|
+
let settingsTab = 'ticket';
|
|
129
130
|
/** @type {string} */
|
|
130
131
|
let currentModel = 'claude-sonnet-5';
|
|
132
|
+
/** @type {Array<{id:string,label?:string,name?:string}>} */
|
|
133
|
+
let availableModels = [];
|
|
131
134
|
/** @type {'github' | 'jira'} */
|
|
132
135
|
let ticketSource = 'github';
|
|
133
136
|
/** @type {{
|
|
@@ -147,6 +150,19 @@ let ticketSource = 'github';
|
|
|
147
150
|
* jiraApiTokenMasked?: string | null,
|
|
148
151
|
* jiraConfigured?: boolean,
|
|
149
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,
|
|
150
166
|
* }} */
|
|
151
167
|
let appConfig = {};
|
|
152
168
|
|
|
@@ -157,7 +173,7 @@ const els = {
|
|
|
157
173
|
repoName: document.getElementById('repo-name'),
|
|
158
174
|
repoBranch: document.getElementById('repo-branch'),
|
|
159
175
|
ticketSourceMeta: document.getElementById('ticket-source-meta'),
|
|
160
|
-
|
|
176
|
+
modelCurrent: document.getElementById('model-current'),
|
|
161
177
|
navBadgeOverview: document.getElementById('nav-badge-overview'),
|
|
162
178
|
navBadgeRuns: document.getElementById('nav-badge-runs'),
|
|
163
179
|
navBadgeReview: document.getElementById('nav-badge-review'),
|
|
@@ -221,6 +237,7 @@ const els = {
|
|
|
221
237
|
settingsForm: document.getElementById('settings-form'),
|
|
222
238
|
settingsBaseBranch: document.getElementById('settings-base-branch'),
|
|
223
239
|
settingsModel: document.getElementById('settings-model'),
|
|
240
|
+
settingsModelHint: document.getElementById('settings-model-hint'),
|
|
224
241
|
settingsMaxTurns: document.getElementById('settings-max-turns'),
|
|
225
242
|
settingsTimeout: document.getElementById('settings-timeout'),
|
|
226
243
|
settingsTimeoutMs: document.getElementById('settings-timeout-ms'),
|
|
@@ -238,13 +255,30 @@ const els = {
|
|
|
238
255
|
jiraTestBtn: document.getElementById('jira-test-btn'),
|
|
239
256
|
jiraStatus: document.getElementById('jira-status'),
|
|
240
257
|
settingsJiraRuleEnabled: document.getElementById('settings-jira-rule-enabled'),
|
|
258
|
+
settingsJiraRuleAction: document.getElementById('settings-jira-rule-action'),
|
|
241
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'),
|
|
242
263
|
settingsGithubRuleEnabled: document.getElementById('settings-github-rule-enabled'),
|
|
243
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'),
|
|
244
267
|
settingsGithubRuleLabel: document.getElementById('settings-github-rule-label'),
|
|
245
268
|
settingsGithubRuleLabelField: document.getElementById('settings-github-rule-label-field'),
|
|
246
|
-
|
|
247
|
-
|
|
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'),
|
|
248
282
|
};
|
|
249
283
|
|
|
250
284
|
function loadDismissed() {
|
|
@@ -2890,13 +2924,94 @@ async function readJson(res) {
|
|
|
2890
2924
|
}
|
|
2891
2925
|
}
|
|
2892
2926
|
|
|
2893
|
-
function
|
|
2894
|
-
els.
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
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
|
+
});
|
|
2900
3015
|
}
|
|
2901
3016
|
}
|
|
2902
3017
|
|
|
@@ -2924,9 +3039,160 @@ function knownToolsList(cfg = appConfig) {
|
|
|
2924
3039
|
: DEFAULT_KNOWN_TOOLS;
|
|
2925
3040
|
}
|
|
2926
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
|
+
|
|
2927
3191
|
function fillSettingsForm(cfg) {
|
|
2928
3192
|
if (!els.settingsForm || !cfg) return;
|
|
2929
3193
|
|
|
3194
|
+
fillAuthSettings(cfg);
|
|
3195
|
+
|
|
2930
3196
|
updateTicketSourceUI(cfg.ticketSource === 'jira' ? 'jira' : 'github');
|
|
2931
3197
|
|
|
2932
3198
|
if (els.settingsJiraBaseUrl) {
|
|
@@ -2954,24 +3220,37 @@ function fillSettingsForm(cfg) {
|
|
|
2954
3220
|
if (els.settingsJiraRuleEnabled) {
|
|
2955
3221
|
els.settingsJiraRuleEnabled.checked = Boolean(jiraRule.enabled);
|
|
2956
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
|
+
}
|
|
2957
3229
|
if (els.settingsJiraRuleStatus) {
|
|
2958
3230
|
els.settingsJiraRuleStatus.value = jiraRule.targetStatus || 'In Review';
|
|
2959
3231
|
}
|
|
3232
|
+
if (els.settingsJiraRuleLabel) {
|
|
3233
|
+
els.settingsJiraRuleLabel.value = jiraRule.label || '';
|
|
3234
|
+
}
|
|
2960
3235
|
|
|
2961
3236
|
const ghRule = cfg.githubRules?.afterPrOpened || {};
|
|
2962
3237
|
if (els.settingsGithubRuleEnabled) {
|
|
2963
3238
|
els.settingsGithubRuleEnabled.checked = Boolean(ghRule.enabled);
|
|
2964
3239
|
}
|
|
2965
3240
|
if (els.settingsGithubRuleAction) {
|
|
2966
|
-
const action = ['none', 'add_label', 'close_issue'].includes(ghRule.action)
|
|
3241
|
+
const action = ['none', 'set_status', 'add_label', 'close_issue'].includes(ghRule.action)
|
|
2967
3242
|
? ghRule.action
|
|
2968
3243
|
: 'none';
|
|
2969
3244
|
els.settingsGithubRuleAction.value = action;
|
|
2970
3245
|
}
|
|
3246
|
+
if (els.settingsGithubRuleStatus) {
|
|
3247
|
+
els.settingsGithubRuleStatus.value = ghRule.targetStatus || 'In Review';
|
|
3248
|
+
}
|
|
2971
3249
|
if (els.settingsGithubRuleLabel) {
|
|
2972
3250
|
els.settingsGithubRuleLabel.value = ghRule.label || '';
|
|
2973
3251
|
}
|
|
2974
|
-
|
|
3252
|
+
updateJiraRuleFieldsVisibility();
|
|
3253
|
+
updateGithubRuleFieldsVisibility();
|
|
2975
3254
|
|
|
2976
3255
|
if (els.jiraStatus) {
|
|
2977
3256
|
els.jiraStatus.textContent = cfg.jiraConfigured
|
|
@@ -2987,19 +3266,15 @@ function fillSettingsForm(cfg) {
|
|
|
2987
3266
|
}
|
|
2988
3267
|
|
|
2989
3268
|
if (els.settingsModel) {
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
opt.value = m.id;
|
|
2998
|
-
opt.textContent = m.label || m.id;
|
|
2999
|
-
els.settingsModel.appendChild(opt);
|
|
3000
|
-
}
|
|
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
|
+
});
|
|
3001
3276
|
}
|
|
3002
|
-
|
|
3277
|
+
void fetchModels();
|
|
3003
3278
|
}
|
|
3004
3279
|
|
|
3005
3280
|
if (els.settingsMaxTurns) {
|
|
@@ -3041,12 +3316,39 @@ function fillSettingsForm(cfg) {
|
|
|
3041
3316
|
}
|
|
3042
3317
|
|
|
3043
3318
|
/**
|
|
3044
|
-
* Show
|
|
3319
|
+
* Show status / label fields based on the selected post-PR action.
|
|
3320
|
+
* @param {'jira' | 'github'} source
|
|
3045
3321
|
*/
|
|
3046
|
-
function
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
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.
|
|
3349
|
+
*/
|
|
3350
|
+
function updateGithubRuleFieldsVisibility() {
|
|
3351
|
+
updateRuleFieldsVisibility('github');
|
|
3050
3352
|
}
|
|
3051
3353
|
|
|
3052
3354
|
/**
|
|
@@ -3066,12 +3368,8 @@ function updateTicketSourceUI(source) {
|
|
|
3066
3368
|
els.jiraConnectPanel.classList.toggle('hidden', ticketSource !== 'jira');
|
|
3067
3369
|
}
|
|
3068
3370
|
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
}
|
|
3072
|
-
if (els.githubRulesPanel) {
|
|
3073
|
-
els.githubRulesPanel.classList.toggle('hidden', ticketSource !== 'github');
|
|
3074
|
-
}
|
|
3371
|
+
// Rules tab always shows both GitHub and Jira post-PR options
|
|
3372
|
+
// (ticket source only gates the Jira connect panel above).
|
|
3075
3373
|
|
|
3076
3374
|
if (els.ticketSourceMeta) {
|
|
3077
3375
|
els.ticketSourceMeta.textContent =
|
|
@@ -3121,7 +3419,7 @@ function updateTimeoutMsHint(minutes) {
|
|
|
3121
3419
|
function applyConfigSnapshot(data) {
|
|
3122
3420
|
appConfig = data || {};
|
|
3123
3421
|
if (data?.model) currentModel = data.model;
|
|
3124
|
-
|
|
3422
|
+
updateModelLabel();
|
|
3125
3423
|
updateTicketSourceUI(data?.ticketSource === 'jira' ? 'jira' : 'github');
|
|
3126
3424
|
if (els.repoName) {
|
|
3127
3425
|
els.repoName.textContent = data?.repoName || 'local repo';
|
|
@@ -3145,35 +3443,6 @@ async function fetchConfig() {
|
|
|
3145
3443
|
}
|
|
3146
3444
|
}
|
|
3147
3445
|
|
|
3148
|
-
async function setModel(model) {
|
|
3149
|
-
if (modelSaving || model === currentModel) return;
|
|
3150
|
-
modelSaving = true;
|
|
3151
|
-
const prev = currentModel;
|
|
3152
|
-
currentModel = model;
|
|
3153
|
-
updateModelButtons();
|
|
3154
|
-
try {
|
|
3155
|
-
const res = await fetch('/api/config', {
|
|
3156
|
-
method: 'PATCH',
|
|
3157
|
-
headers: { 'Content-Type': 'application/json' },
|
|
3158
|
-
body: JSON.stringify({ model }),
|
|
3159
|
-
});
|
|
3160
|
-
const data = await readJson(res);
|
|
3161
|
-
if (!res.ok) {
|
|
3162
|
-
currentModel = prev;
|
|
3163
|
-
updateModelButtons();
|
|
3164
|
-
alert(`Model update failed: ${data.error || `HTTP ${res.status}`}`);
|
|
3165
|
-
return;
|
|
3166
|
-
}
|
|
3167
|
-
applyConfigSnapshot({ ...appConfig, ...data });
|
|
3168
|
-
} catch (err) {
|
|
3169
|
-
currentModel = prev;
|
|
3170
|
-
updateModelButtons();
|
|
3171
|
-
alert(`Model update failed: ${err.message}`);
|
|
3172
|
-
} finally {
|
|
3173
|
-
modelSaving = false;
|
|
3174
|
-
}
|
|
3175
|
-
}
|
|
3176
|
-
|
|
3177
3446
|
function readSettingsForm() {
|
|
3178
3447
|
const baseBranch = els.settingsBaseBranch?.value?.trim() || '';
|
|
3179
3448
|
const model = els.settingsModel?.value || currentModel;
|
|
@@ -3197,6 +3466,13 @@ function readSettingsForm() {
|
|
|
3197
3466
|
ticketSource,
|
|
3198
3467
|
};
|
|
3199
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
|
+
|
|
3200
3476
|
if (ticketSource === 'jira' || els.settingsJiraBaseUrl?.value) {
|
|
3201
3477
|
patch.jiraBaseUrl = els.settingsJiraBaseUrl?.value?.trim() || '';
|
|
3202
3478
|
patch.jiraPrLinkPhrase =
|
|
@@ -3207,28 +3483,31 @@ function readSettingsForm() {
|
|
|
3207
3483
|
if (token) patch.jiraApiToken = token;
|
|
3208
3484
|
}
|
|
3209
3485
|
|
|
3210
|
-
//
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
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
|
+
};
|
|
3232
3511
|
|
|
3233
3512
|
return patch;
|
|
3234
3513
|
}
|
|
@@ -3267,6 +3546,7 @@ async function saveSettings(event) {
|
|
|
3267
3546
|
}
|
|
3268
3547
|
applyConfigSnapshot(data);
|
|
3269
3548
|
fillSettingsForm(data);
|
|
3549
|
+
void fetchModels({ refresh: true });
|
|
3270
3550
|
setSettingsFeedback('Settings saved. Next jobs will use these values.', 'ok');
|
|
3271
3551
|
} catch (err) {
|
|
3272
3552
|
setSettingsFeedback(err.message || 'Save failed', 'error');
|
|
@@ -3286,15 +3566,79 @@ els.themeToggle.addEventListener('click', () => {
|
|
|
3286
3566
|
applyTheme(next);
|
|
3287
3567
|
});
|
|
3288
3568
|
|
|
3289
|
-
els.modelToggle.querySelectorAll('.model-btn').forEach((btn) => {
|
|
3290
|
-
btn.addEventListener('click', () => setModel(btn.dataset.model));
|
|
3291
|
-
});
|
|
3292
|
-
|
|
3293
3569
|
els.settingsForm?.addEventListener('submit', saveSettings);
|
|
3294
3570
|
els.settingsTimeout?.addEventListener('input', () => {
|
|
3295
3571
|
updateTimeoutMsHint(els.settingsTimeout.value);
|
|
3296
3572
|
});
|
|
3297
|
-
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
|
+
);
|
|
3298
3642
|
|
|
3299
3643
|
els.addBtn.addEventListener('click', async () => {
|
|
3300
3644
|
els.enqueueFeedback.classList.add('hidden');
|
|
@@ -3615,5 +3959,6 @@ els.logJobFilter.addEventListener('change', () => {
|
|
|
3615
3959
|
|
|
3616
3960
|
applyTheme(loadTheme());
|
|
3617
3961
|
fetchConfig();
|
|
3962
|
+
fetchModels();
|
|
3618
3963
|
fetchJobs();
|
|
3619
3964
|
setInterval(fetchJobs, 3000);
|