acdev 1.0.2 → 1.0.4
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 +66 -24
- package/bin/acdev.js +13 -25
- package/package.json +1 -1
- package/public/app.js +469 -93
- package/public/index.html +201 -63
- package/public/styles.css +105 -21
- package/src/afterPrRules.js +63 -8
- package/src/claude-auth.js +30 -0
- package/src/config.js +130 -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 +142 -13
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,20 @@ 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,
|
|
166
|
+
* stubAgent?: boolean,
|
|
150
167
|
* }} */
|
|
151
168
|
let appConfig = {};
|
|
152
169
|
|
|
@@ -157,11 +174,14 @@ const els = {
|
|
|
157
174
|
repoName: document.getElementById('repo-name'),
|
|
158
175
|
repoBranch: document.getElementById('repo-branch'),
|
|
159
176
|
ticketSourceMeta: document.getElementById('ticket-source-meta'),
|
|
160
|
-
|
|
177
|
+
modelCurrent: document.getElementById('model-current'),
|
|
161
178
|
navBadgeOverview: document.getElementById('nav-badge-overview'),
|
|
162
179
|
navBadgeRuns: document.getElementById('nav-badge-runs'),
|
|
163
180
|
navBadgeReview: document.getElementById('nav-badge-review'),
|
|
164
181
|
navBadgeAlerts: document.getElementById('nav-badge-alerts'),
|
|
182
|
+
authBanner: document.getElementById('auth-banner'),
|
|
183
|
+
authBannerText: document.getElementById('auth-banner-text'),
|
|
184
|
+
authBannerBtn: document.getElementById('auth-banner-btn'),
|
|
165
185
|
issueUrls: document.getElementById('issue-urls'),
|
|
166
186
|
issueUrlsLabel: document.getElementById('issue-urls-label'),
|
|
167
187
|
enqueueFeedback: document.getElementById('enqueue-feedback'),
|
|
@@ -221,6 +241,7 @@ const els = {
|
|
|
221
241
|
settingsForm: document.getElementById('settings-form'),
|
|
222
242
|
settingsBaseBranch: document.getElementById('settings-base-branch'),
|
|
223
243
|
settingsModel: document.getElementById('settings-model'),
|
|
244
|
+
settingsModelHint: document.getElementById('settings-model-hint'),
|
|
224
245
|
settingsMaxTurns: document.getElementById('settings-max-turns'),
|
|
225
246
|
settingsTimeout: document.getElementById('settings-timeout'),
|
|
226
247
|
settingsTimeoutMs: document.getElementById('settings-timeout-ms'),
|
|
@@ -238,13 +259,30 @@ const els = {
|
|
|
238
259
|
jiraTestBtn: document.getElementById('jira-test-btn'),
|
|
239
260
|
jiraStatus: document.getElementById('jira-status'),
|
|
240
261
|
settingsJiraRuleEnabled: document.getElementById('settings-jira-rule-enabled'),
|
|
262
|
+
settingsJiraRuleAction: document.getElementById('settings-jira-rule-action'),
|
|
241
263
|
settingsJiraRuleStatus: document.getElementById('settings-jira-rule-status'),
|
|
264
|
+
settingsJiraRuleStatusField: document.getElementById('settings-jira-rule-status-field'),
|
|
265
|
+
settingsJiraRuleLabel: document.getElementById('settings-jira-rule-label'),
|
|
266
|
+
settingsJiraRuleLabelField: document.getElementById('settings-jira-rule-label-field'),
|
|
242
267
|
settingsGithubRuleEnabled: document.getElementById('settings-github-rule-enabled'),
|
|
243
268
|
settingsGithubRuleAction: document.getElementById('settings-github-rule-action'),
|
|
269
|
+
settingsGithubRuleStatus: document.getElementById('settings-github-rule-status'),
|
|
270
|
+
settingsGithubRuleStatusField: document.getElementById('settings-github-rule-status-field'),
|
|
244
271
|
settingsGithubRuleLabel: document.getElementById('settings-github-rule-label'),
|
|
245
272
|
settingsGithubRuleLabelField: document.getElementById('settings-github-rule-label-field'),
|
|
246
|
-
|
|
247
|
-
|
|
273
|
+
settingsGhStatus: document.getElementById('settings-gh-status'),
|
|
274
|
+
settingsGhOrigin: document.getElementById('settings-gh-origin'),
|
|
275
|
+
settingsGhToken: document.getElementById('settings-gh-token'),
|
|
276
|
+
settingsGhTokenClear: document.getElementById('settings-gh-token-clear'),
|
|
277
|
+
settingsGhTokenHint: document.getElementById('settings-gh-token-hint'),
|
|
278
|
+
settingsClaudeStatus: document.getElementById('settings-claude-status'),
|
|
279
|
+
settingsAnthropicKey: document.getElementById('settings-anthropic-key'),
|
|
280
|
+
settingsAnthropicKeyClear: document.getElementById('settings-anthropic-key-clear'),
|
|
281
|
+
settingsAnthropicKeyHint: document.getElementById('settings-anthropic-key-hint'),
|
|
282
|
+
settingsClaudeOauth: document.getElementById('settings-claude-oauth'),
|
|
283
|
+
settingsClaudeOauthClear: document.getElementById('settings-claude-oauth-clear'),
|
|
284
|
+
settingsClaudeOauthHint: document.getElementById('settings-claude-oauth-hint'),
|
|
285
|
+
settingsTabs: document.getElementById('settings-tabs'),
|
|
248
286
|
};
|
|
249
287
|
|
|
250
288
|
function loadDismissed() {
|
|
@@ -2890,13 +2928,94 @@ async function readJson(res) {
|
|
|
2890
2928
|
}
|
|
2891
2929
|
}
|
|
2892
2930
|
|
|
2893
|
-
function
|
|
2894
|
-
els.
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2931
|
+
function updateModelLabel() {
|
|
2932
|
+
if (!els.modelCurrent) return;
|
|
2933
|
+
const id = currentModel || '—';
|
|
2934
|
+
els.modelCurrent.textContent = id;
|
|
2935
|
+
els.modelCurrent.title = `Model: ${id} — change in Settings → Configuration`;
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
/**
|
|
2939
|
+
* @param {'anthropic' | 'fallback' | string | undefined} source
|
|
2940
|
+
*/
|
|
2941
|
+
function updateModelSourceHint(source) {
|
|
2942
|
+
if (!els.settingsModelHint) return;
|
|
2943
|
+
if (source === 'anthropic') {
|
|
2944
|
+
els.settingsModelHint.textContent =
|
|
2945
|
+
'Claude model for agent runs. List loaded from Anthropic Models API.';
|
|
2946
|
+
} else if (source === 'fallback') {
|
|
2947
|
+
els.settingsModelHint.textContent =
|
|
2948
|
+
'Claude model for agent runs. Showing curated Claude Code models (live list unavailable — set an API key or use claude auth login).';
|
|
2949
|
+
} else {
|
|
2950
|
+
els.settingsModelHint.textContent = 'Claude model for agent runs.';
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
/**
|
|
2955
|
+
* @param {{ models?: Array<{id:string,label?:string,name?:string}>, selected?: string, source?: string }} data
|
|
2956
|
+
*/
|
|
2957
|
+
function populateSettingsModels(data) {
|
|
2958
|
+
const models = Array.isArray(data?.models) && data.models.length
|
|
2959
|
+
? data.models
|
|
2960
|
+
: availableModels.length
|
|
2961
|
+
? availableModels
|
|
2962
|
+
: [
|
|
2963
|
+
{ id: 'claude-sonnet-5', label: 'Sonnet 5' },
|
|
2964
|
+
{ id: 'claude-opus-5', label: 'Opus 5' },
|
|
2965
|
+
{ id: 'claude-fable-5', label: 'Fable 5' },
|
|
2966
|
+
{ id: 'claude-haiku-4-5', label: 'Haiku 4.5' },
|
|
2967
|
+
];
|
|
2968
|
+
availableModels = models;
|
|
2969
|
+
if (data?.selected) currentModel = data.selected;
|
|
2970
|
+
if (data?.source) updateModelSourceHint(data.source);
|
|
2971
|
+
|
|
2972
|
+
if (els.settingsModel) {
|
|
2973
|
+
const currentIds = [...els.settingsModel.options].map((o) => o.value);
|
|
2974
|
+
const nextIds = models.map((m) => m.id);
|
|
2975
|
+
if (currentIds.join(',') !== nextIds.join(',')) {
|
|
2976
|
+
els.settingsModel.innerHTML = '';
|
|
2977
|
+
for (const m of models) {
|
|
2978
|
+
const opt = document.createElement('option');
|
|
2979
|
+
opt.value = m.id;
|
|
2980
|
+
opt.textContent = m.label || m.name || m.id;
|
|
2981
|
+
els.settingsModel.appendChild(opt);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
const selected = currentModel;
|
|
2985
|
+
if (selected && [...els.settingsModel.options].some((o) => o.value === selected)) {
|
|
2986
|
+
els.settingsModel.value = selected;
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
updateModelLabel();
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
async function fetchModels(opts = {}) {
|
|
2993
|
+
const refresh = opts.refresh === true;
|
|
2994
|
+
try {
|
|
2995
|
+
const res = await fetch(refresh ? '/api/models?refresh=1' : '/api/models');
|
|
2996
|
+
if (!res.ok) {
|
|
2997
|
+
populateSettingsModels({
|
|
2998
|
+
models: availableModels.length
|
|
2999
|
+
? availableModels
|
|
3000
|
+
: [
|
|
3001
|
+
{ id: currentModel || 'claude-sonnet-5', label: currentModel || 'claude-sonnet-5' },
|
|
3002
|
+
],
|
|
3003
|
+
selected: currentModel,
|
|
3004
|
+
});
|
|
3005
|
+
return;
|
|
3006
|
+
}
|
|
3007
|
+
const data = await readJson(res);
|
|
3008
|
+
populateSettingsModels(data);
|
|
3009
|
+
} catch (err) {
|
|
3010
|
+
console.error('Failed to fetch models:', err);
|
|
3011
|
+
populateSettingsModels({
|
|
3012
|
+
models: availableModels.length
|
|
3013
|
+
? availableModels
|
|
3014
|
+
: [
|
|
3015
|
+
{ id: currentModel || 'claude-sonnet-5', label: currentModel || 'claude-sonnet-5' },
|
|
3016
|
+
],
|
|
3017
|
+
selected: currentModel,
|
|
3018
|
+
});
|
|
2900
3019
|
}
|
|
2901
3020
|
}
|
|
2902
3021
|
|
|
@@ -2924,9 +3043,160 @@ function knownToolsList(cfg = appConfig) {
|
|
|
2924
3043
|
: DEFAULT_KNOWN_TOOLS;
|
|
2925
3044
|
}
|
|
2926
3045
|
|
|
3046
|
+
/**
|
|
3047
|
+
* Switch Settings top-level tab (Ticket source / Authentication / Rules / Configuration).
|
|
3048
|
+
* @param {'ticket' | 'auth' | 'rules' | 'config'} tab
|
|
3049
|
+
*/
|
|
3050
|
+
function setSettingsTab(tab) {
|
|
3051
|
+
const next =
|
|
3052
|
+
tab === 'auth' || tab === 'rules' || tab === 'config' ? tab : 'ticket';
|
|
3053
|
+
settingsTab = next;
|
|
3054
|
+
|
|
3055
|
+
if (els.settingsTabs) {
|
|
3056
|
+
els.settingsTabs.querySelectorAll('[role="tab"]').forEach((btn) => {
|
|
3057
|
+
const selected = btn.dataset.settingsTab === next;
|
|
3058
|
+
btn.setAttribute('aria-selected', selected ? 'true' : 'false');
|
|
3059
|
+
btn.tabIndex = selected ? 0 : -1;
|
|
3060
|
+
});
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
document.querySelectorAll('[data-settings-panel]').forEach((panel) => {
|
|
3064
|
+
const show = panel.dataset.settingsPanel === next;
|
|
3065
|
+
panel.classList.toggle('hidden', !show);
|
|
3066
|
+
});
|
|
3067
|
+
|
|
3068
|
+
if (next === 'config') {
|
|
3069
|
+
void fetchModels();
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
/**
|
|
3074
|
+
* Empty = keep existing secret; data-clear + empty = send null to clear.
|
|
3075
|
+
* @param {HTMLInputElement | null} inputEl
|
|
3076
|
+
* @returns {string | null | undefined}
|
|
3077
|
+
*/
|
|
3078
|
+
function secretPatchValue(inputEl) {
|
|
3079
|
+
const typed = inputEl?.value?.trim() || '';
|
|
3080
|
+
if (typed) return typed;
|
|
3081
|
+
if (inputEl?.dataset.clear === '1') return null;
|
|
3082
|
+
return undefined;
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
/**
|
|
3086
|
+
* @param {HTMLInputElement | null} inputEl
|
|
3087
|
+
* @param {HTMLButtonElement | null} clearBtn
|
|
3088
|
+
* @param {boolean} set
|
|
3089
|
+
* @param {string | null | undefined} masked
|
|
3090
|
+
* @param {string} unsetPlaceholder
|
|
3091
|
+
*/
|
|
3092
|
+
function fillSecretInput(inputEl, clearBtn, set, masked, unsetPlaceholder) {
|
|
3093
|
+
if (!inputEl) return;
|
|
3094
|
+
inputEl.value = '';
|
|
3095
|
+
delete inputEl.dataset.clear;
|
|
3096
|
+
inputEl.placeholder = set
|
|
3097
|
+
? `Saved (${masked || '••••'}) — leave blank to keep`
|
|
3098
|
+
: unsetPlaceholder;
|
|
3099
|
+
if (clearBtn) {
|
|
3100
|
+
clearBtn.classList.toggle('hidden', !set);
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
function ghStatusText(cfg) {
|
|
3105
|
+
if (!cfg.ghInstalled) {
|
|
3106
|
+
return 'gh is not installed (required for issues and PRs)';
|
|
3107
|
+
}
|
|
3108
|
+
if (cfg.ghAuthOk) {
|
|
3109
|
+
const method =
|
|
3110
|
+
cfg.ghAuthMethod === 'token'
|
|
3111
|
+
? 'env token (GH_TOKEN)'
|
|
3112
|
+
: 'gh auth login';
|
|
3113
|
+
return `Authenticated via ${method}`;
|
|
3114
|
+
}
|
|
3115
|
+
if (cfg.ghTokenSet) {
|
|
3116
|
+
return 'Token saved, but gh is not authenticated — check the token';
|
|
3117
|
+
}
|
|
3118
|
+
return 'Not authenticated — paste a PAT or run gh auth login';
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
function claudeStatusText(cfg) {
|
|
3122
|
+
switch (cfg.claudeAuthMethod) {
|
|
3123
|
+
case 'api-key':
|
|
3124
|
+
return 'Authenticated via ANTHROPIC_API_KEY (API billing)';
|
|
3125
|
+
case 'auth-token':
|
|
3126
|
+
return 'Authenticated via ANTHROPIC_AUTH_TOKEN';
|
|
3127
|
+
case 'oauth-token':
|
|
3128
|
+
return 'Authenticated via CLAUDE_CODE_OAUTH_TOKEN';
|
|
3129
|
+
case 'claude-code-login':
|
|
3130
|
+
return 'Authenticated via claude auth login (subscription)';
|
|
3131
|
+
default:
|
|
3132
|
+
return 'Not authenticated — add an API key, OAuth token, or run claude auth login';
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
function fillAuthSettings(cfg) {
|
|
3137
|
+
if (els.settingsGhStatus) {
|
|
3138
|
+
els.settingsGhStatus.textContent = ghStatusText(cfg);
|
|
3139
|
+
els.settingsGhStatus.className =
|
|
3140
|
+
cfg.ghAuthOk ? 'auth-status ok' : 'auth-status err';
|
|
3141
|
+
}
|
|
3142
|
+
if (els.settingsGhOrigin) {
|
|
3143
|
+
if (cfg.originRemoteUrl) {
|
|
3144
|
+
els.settingsGhOrigin.textContent = cfg.originRemoteSsh
|
|
3145
|
+
? `origin: ${cfg.originRemoteUrl} (SSH — git push/fetch only, not gh API auth)`
|
|
3146
|
+
: `origin: ${cfg.originRemoteUrl}`;
|
|
3147
|
+
} else {
|
|
3148
|
+
els.settingsGhOrigin.textContent = '';
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
fillSecretInput(
|
|
3152
|
+
els.settingsGhToken,
|
|
3153
|
+
els.settingsGhTokenClear,
|
|
3154
|
+
Boolean(cfg.ghTokenSet),
|
|
3155
|
+
cfg.ghTokenMasked,
|
|
3156
|
+
'Paste a GitHub PAT'
|
|
3157
|
+
);
|
|
3158
|
+
if (els.settingsGhTokenHint) {
|
|
3159
|
+
els.settingsGhTokenHint.innerHTML = cfg.ghTokenSet
|
|
3160
|
+
? '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.'
|
|
3161
|
+
: '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.';
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
if (els.settingsClaudeStatus) {
|
|
3165
|
+
els.settingsClaudeStatus.textContent = claudeStatusText(cfg);
|
|
3166
|
+
els.settingsClaudeStatus.className =
|
|
3167
|
+
cfg.claudeAuthOk ? 'auth-status ok' : 'auth-status err';
|
|
3168
|
+
}
|
|
3169
|
+
fillSecretInput(
|
|
3170
|
+
els.settingsAnthropicKey,
|
|
3171
|
+
els.settingsAnthropicKeyClear,
|
|
3172
|
+
Boolean(cfg.anthropicApiKeySet),
|
|
3173
|
+
cfg.anthropicApiKeyMasked,
|
|
3174
|
+
'Paste an Anthropic API key'
|
|
3175
|
+
);
|
|
3176
|
+
fillSecretInput(
|
|
3177
|
+
els.settingsClaudeOauth,
|
|
3178
|
+
els.settingsClaudeOauthClear,
|
|
3179
|
+
Boolean(cfg.claudeOauthTokenSet),
|
|
3180
|
+
cfg.claudeOauthTokenMasked,
|
|
3181
|
+
'Paste token from claude setup-token'
|
|
3182
|
+
);
|
|
3183
|
+
if (els.settingsAnthropicKeyHint) {
|
|
3184
|
+
els.settingsAnthropicKeyHint.innerHTML = cfg.anthropicApiKeySet
|
|
3185
|
+
? 'API key stored in <code>.acdev/.env</code> (not committed). Takes precedence over subscription login — clear it to use <code>claude auth login</code>.'
|
|
3186
|
+
: '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.';
|
|
3187
|
+
}
|
|
3188
|
+
if (els.settingsClaudeOauthHint) {
|
|
3189
|
+
els.settingsClaudeOauthHint.innerHTML = cfg.claudeOauthTokenSet
|
|
3190
|
+
? '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.'
|
|
3191
|
+
: '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.';
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
|
|
2927
3195
|
function fillSettingsForm(cfg) {
|
|
2928
3196
|
if (!els.settingsForm || !cfg) return;
|
|
2929
3197
|
|
|
3198
|
+
fillAuthSettings(cfg);
|
|
3199
|
+
|
|
2930
3200
|
updateTicketSourceUI(cfg.ticketSource === 'jira' ? 'jira' : 'github');
|
|
2931
3201
|
|
|
2932
3202
|
if (els.settingsJiraBaseUrl) {
|
|
@@ -2954,24 +3224,37 @@ function fillSettingsForm(cfg) {
|
|
|
2954
3224
|
if (els.settingsJiraRuleEnabled) {
|
|
2955
3225
|
els.settingsJiraRuleEnabled.checked = Boolean(jiraRule.enabled);
|
|
2956
3226
|
}
|
|
3227
|
+
if (els.settingsJiraRuleAction) {
|
|
3228
|
+
const action = ['none', 'set_status', 'add_label', 'close_issue'].includes(jiraRule.action)
|
|
3229
|
+
? jiraRule.action
|
|
3230
|
+
: 'none';
|
|
3231
|
+
els.settingsJiraRuleAction.value = action;
|
|
3232
|
+
}
|
|
2957
3233
|
if (els.settingsJiraRuleStatus) {
|
|
2958
3234
|
els.settingsJiraRuleStatus.value = jiraRule.targetStatus || 'In Review';
|
|
2959
3235
|
}
|
|
3236
|
+
if (els.settingsJiraRuleLabel) {
|
|
3237
|
+
els.settingsJiraRuleLabel.value = jiraRule.label || '';
|
|
3238
|
+
}
|
|
2960
3239
|
|
|
2961
3240
|
const ghRule = cfg.githubRules?.afterPrOpened || {};
|
|
2962
3241
|
if (els.settingsGithubRuleEnabled) {
|
|
2963
3242
|
els.settingsGithubRuleEnabled.checked = Boolean(ghRule.enabled);
|
|
2964
3243
|
}
|
|
2965
3244
|
if (els.settingsGithubRuleAction) {
|
|
2966
|
-
const action = ['none', 'add_label', 'close_issue'].includes(ghRule.action)
|
|
3245
|
+
const action = ['none', 'set_status', 'add_label', 'close_issue'].includes(ghRule.action)
|
|
2967
3246
|
? ghRule.action
|
|
2968
3247
|
: 'none';
|
|
2969
3248
|
els.settingsGithubRuleAction.value = action;
|
|
2970
3249
|
}
|
|
3250
|
+
if (els.settingsGithubRuleStatus) {
|
|
3251
|
+
els.settingsGithubRuleStatus.value = ghRule.targetStatus || 'In Review';
|
|
3252
|
+
}
|
|
2971
3253
|
if (els.settingsGithubRuleLabel) {
|
|
2972
3254
|
els.settingsGithubRuleLabel.value = ghRule.label || '';
|
|
2973
3255
|
}
|
|
2974
|
-
|
|
3256
|
+
updateJiraRuleFieldsVisibility();
|
|
3257
|
+
updateGithubRuleFieldsVisibility();
|
|
2975
3258
|
|
|
2976
3259
|
if (els.jiraStatus) {
|
|
2977
3260
|
els.jiraStatus.textContent = cfg.jiraConfigured
|
|
@@ -2987,19 +3270,15 @@ function fillSettingsForm(cfg) {
|
|
|
2987
3270
|
}
|
|
2988
3271
|
|
|
2989
3272
|
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
|
-
}
|
|
3273
|
+
if (availableModels.length) {
|
|
3274
|
+
populateSettingsModels({ models: availableModels, selected: cfg.model || currentModel });
|
|
3275
|
+
} else if (cfg.model) {
|
|
3276
|
+
populateSettingsModels({
|
|
3277
|
+
models: [{ id: cfg.model, label: cfg.model }],
|
|
3278
|
+
selected: cfg.model,
|
|
3279
|
+
});
|
|
3001
3280
|
}
|
|
3002
|
-
|
|
3281
|
+
void fetchModels();
|
|
3003
3282
|
}
|
|
3004
3283
|
|
|
3005
3284
|
if (els.settingsMaxTurns) {
|
|
@@ -3041,12 +3320,39 @@ function fillSettingsForm(cfg) {
|
|
|
3041
3320
|
}
|
|
3042
3321
|
|
|
3043
3322
|
/**
|
|
3044
|
-
* Show
|
|
3323
|
+
* Show status / label fields based on the selected post-PR action.
|
|
3324
|
+
* @param {'jira' | 'github'} source
|
|
3045
3325
|
*/
|
|
3046
|
-
function
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3326
|
+
function updateRuleFieldsVisibility(source) {
|
|
3327
|
+
const actionEl =
|
|
3328
|
+
source === 'jira' ? els.settingsJiraRuleAction : els.settingsGithubRuleAction;
|
|
3329
|
+
const statusField =
|
|
3330
|
+
source === 'jira'
|
|
3331
|
+
? els.settingsJiraRuleStatusField
|
|
3332
|
+
: els.settingsGithubRuleStatusField;
|
|
3333
|
+
const labelField =
|
|
3334
|
+
source === 'jira'
|
|
3335
|
+
? els.settingsJiraRuleLabelField
|
|
3336
|
+
: els.settingsGithubRuleLabelField;
|
|
3337
|
+
if (!actionEl) return;
|
|
3338
|
+
const action = actionEl.value;
|
|
3339
|
+
if (statusField) {
|
|
3340
|
+
statusField.classList.toggle('hidden', action !== 'set_status');
|
|
3341
|
+
}
|
|
3342
|
+
if (labelField) {
|
|
3343
|
+
labelField.classList.toggle('hidden', action !== 'add_label');
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
function updateJiraRuleFieldsVisibility() {
|
|
3348
|
+
updateRuleFieldsVisibility('jira');
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
/**
|
|
3352
|
+
* Show status / label fields based on the selected GitHub post-PR action.
|
|
3353
|
+
*/
|
|
3354
|
+
function updateGithubRuleFieldsVisibility() {
|
|
3355
|
+
updateRuleFieldsVisibility('github');
|
|
3050
3356
|
}
|
|
3051
3357
|
|
|
3052
3358
|
/**
|
|
@@ -3066,12 +3372,8 @@ function updateTicketSourceUI(source) {
|
|
|
3066
3372
|
els.jiraConnectPanel.classList.toggle('hidden', ticketSource !== 'jira');
|
|
3067
3373
|
}
|
|
3068
3374
|
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
}
|
|
3072
|
-
if (els.githubRulesPanel) {
|
|
3073
|
-
els.githubRulesPanel.classList.toggle('hidden', ticketSource !== 'github');
|
|
3074
|
-
}
|
|
3375
|
+
// Rules tab always shows both GitHub and Jira post-PR options
|
|
3376
|
+
// (ticket source only gates the Jira connect panel above).
|
|
3075
3377
|
|
|
3076
3378
|
if (els.ticketSourceMeta) {
|
|
3077
3379
|
els.ticketSourceMeta.textContent =
|
|
@@ -3121,7 +3423,7 @@ function updateTimeoutMsHint(minutes) {
|
|
|
3121
3423
|
function applyConfigSnapshot(data) {
|
|
3122
3424
|
appConfig = data || {};
|
|
3123
3425
|
if (data?.model) currentModel = data.model;
|
|
3124
|
-
|
|
3426
|
+
updateModelLabel();
|
|
3125
3427
|
updateTicketSourceUI(data?.ticketSource === 'jira' ? 'jira' : 'github');
|
|
3126
3428
|
if (els.repoName) {
|
|
3127
3429
|
els.repoName.textContent = data?.repoName || 'local repo';
|
|
@@ -3129,11 +3431,33 @@ function applyConfigSnapshot(data) {
|
|
|
3129
3431
|
if (els.repoBranch) {
|
|
3130
3432
|
els.repoBranch.textContent = data?.baseBranch || '—';
|
|
3131
3433
|
}
|
|
3434
|
+
updateAuthBanner(appConfig);
|
|
3132
3435
|
if (currentView === 'settings') {
|
|
3133
3436
|
fillSettingsForm(appConfig);
|
|
3134
3437
|
}
|
|
3135
3438
|
}
|
|
3136
3439
|
|
|
3440
|
+
/**
|
|
3441
|
+
* Surface missing GitHub / Claude auth on Overview so users can open Settings.
|
|
3442
|
+
* @param {typeof appConfig} cfg
|
|
3443
|
+
*/
|
|
3444
|
+
function updateAuthBanner(cfg) {
|
|
3445
|
+
if (!els.authBanner || !els.authBannerText) return;
|
|
3446
|
+
const missing = [];
|
|
3447
|
+
if (!cfg.ghAuthOk) missing.push('GitHub');
|
|
3448
|
+
if (!cfg.claudeAuthOk && !cfg.stubAgent) missing.push('Claude');
|
|
3449
|
+
if (missing.length === 0) {
|
|
3450
|
+
els.authBanner.classList.add('hidden');
|
|
3451
|
+
els.authBanner.hidden = true;
|
|
3452
|
+
els.authBannerText.textContent = '';
|
|
3453
|
+
return;
|
|
3454
|
+
}
|
|
3455
|
+
const label = missing.join(' and ');
|
|
3456
|
+
els.authBannerText.textContent = `Configure ${label} in Settings → Authentication before starting jobs.`;
|
|
3457
|
+
els.authBanner.classList.remove('hidden');
|
|
3458
|
+
els.authBanner.hidden = false;
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3137
3461
|
async function fetchConfig() {
|
|
3138
3462
|
try {
|
|
3139
3463
|
const res = await fetch('/api/config');
|
|
@@ -3145,35 +3469,6 @@ async function fetchConfig() {
|
|
|
3145
3469
|
}
|
|
3146
3470
|
}
|
|
3147
3471
|
|
|
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
3472
|
function readSettingsForm() {
|
|
3178
3473
|
const baseBranch = els.settingsBaseBranch?.value?.trim() || '';
|
|
3179
3474
|
const model = els.settingsModel?.value || currentModel;
|
|
@@ -3197,6 +3492,13 @@ function readSettingsForm() {
|
|
|
3197
3492
|
ticketSource,
|
|
3198
3493
|
};
|
|
3199
3494
|
|
|
3495
|
+
const ghToken = secretPatchValue(els.settingsGhToken);
|
|
3496
|
+
if (ghToken !== undefined) patch.ghToken = ghToken;
|
|
3497
|
+
const anthropicApiKey = secretPatchValue(els.settingsAnthropicKey);
|
|
3498
|
+
if (anthropicApiKey !== undefined) patch.anthropicApiKey = anthropicApiKey;
|
|
3499
|
+
const claudeOauthToken = secretPatchValue(els.settingsClaudeOauth);
|
|
3500
|
+
if (claudeOauthToken !== undefined) patch.claudeOauthToken = claudeOauthToken;
|
|
3501
|
+
|
|
3200
3502
|
if (ticketSource === 'jira' || els.settingsJiraBaseUrl?.value) {
|
|
3201
3503
|
patch.jiraBaseUrl = els.settingsJiraBaseUrl?.value?.trim() || '';
|
|
3202
3504
|
patch.jiraPrLinkPhrase =
|
|
@@ -3207,28 +3509,31 @@ function readSettingsForm() {
|
|
|
3207
3509
|
if (token) patch.jiraApiToken = token;
|
|
3208
3510
|
}
|
|
3209
3511
|
|
|
3210
|
-
//
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3512
|
+
// Both rule panels are always visible on the Rules tab — PATCH both.
|
|
3513
|
+
const jiraAction = els.settingsJiraRuleAction?.value || 'none';
|
|
3514
|
+
patch.jiraRules = {
|
|
3515
|
+
afterPrOpened: {
|
|
3516
|
+
enabled: Boolean(els.settingsJiraRuleEnabled?.checked),
|
|
3517
|
+
action: ['none', 'set_status', 'add_label', 'close_issue'].includes(jiraAction)
|
|
3518
|
+
? jiraAction
|
|
3519
|
+
: 'none',
|
|
3520
|
+
targetStatus:
|
|
3521
|
+
els.settingsJiraRuleStatus?.value?.trim() || 'In Review',
|
|
3522
|
+
label: els.settingsJiraRuleLabel?.value?.trim() || '',
|
|
3523
|
+
},
|
|
3524
|
+
};
|
|
3525
|
+
const ghAction = els.settingsGithubRuleAction?.value || 'none';
|
|
3526
|
+
patch.githubRules = {
|
|
3527
|
+
afterPrOpened: {
|
|
3528
|
+
enabled: Boolean(els.settingsGithubRuleEnabled?.checked),
|
|
3529
|
+
action: ['none', 'set_status', 'add_label', 'close_issue'].includes(ghAction)
|
|
3530
|
+
? ghAction
|
|
3531
|
+
: 'none',
|
|
3532
|
+
targetStatus:
|
|
3533
|
+
els.settingsGithubRuleStatus?.value?.trim() || 'In Review',
|
|
3534
|
+
label: els.settingsGithubRuleLabel?.value?.trim() || '',
|
|
3535
|
+
},
|
|
3536
|
+
};
|
|
3232
3537
|
|
|
3233
3538
|
return patch;
|
|
3234
3539
|
}
|
|
@@ -3267,6 +3572,7 @@ async function saveSettings(event) {
|
|
|
3267
3572
|
}
|
|
3268
3573
|
applyConfigSnapshot(data);
|
|
3269
3574
|
fillSettingsForm(data);
|
|
3575
|
+
void fetchModels({ refresh: true });
|
|
3270
3576
|
setSettingsFeedback('Settings saved. Next jobs will use these values.', 'ok');
|
|
3271
3577
|
} catch (err) {
|
|
3272
3578
|
setSettingsFeedback(err.message || 'Save failed', 'error');
|
|
@@ -3281,20 +3587,89 @@ document.querySelectorAll('.nav-item').forEach((btn) => {
|
|
|
3281
3587
|
btn.addEventListener('click', () => setView(/** @type {ViewId} */ (btn.dataset.view)));
|
|
3282
3588
|
});
|
|
3283
3589
|
|
|
3590
|
+
els.authBannerBtn?.addEventListener('click', () => {
|
|
3591
|
+
setView('settings');
|
|
3592
|
+
setSettingsTab('auth');
|
|
3593
|
+
});
|
|
3594
|
+
|
|
3284
3595
|
els.themeToggle.addEventListener('click', () => {
|
|
3285
3596
|
const next = els.app.dataset.theme === 'dark' ? 'light' : 'dark';
|
|
3286
3597
|
applyTheme(next);
|
|
3287
3598
|
});
|
|
3288
3599
|
|
|
3289
|
-
els.modelToggle.querySelectorAll('.model-btn').forEach((btn) => {
|
|
3290
|
-
btn.addEventListener('click', () => setModel(btn.dataset.model));
|
|
3291
|
-
});
|
|
3292
|
-
|
|
3293
3600
|
els.settingsForm?.addEventListener('submit', saveSettings);
|
|
3294
3601
|
els.settingsTimeout?.addEventListener('input', () => {
|
|
3295
3602
|
updateTimeoutMsHint(els.settingsTimeout.value);
|
|
3296
3603
|
});
|
|
3297
|
-
els.
|
|
3604
|
+
els.settingsJiraRuleAction?.addEventListener('change', updateJiraRuleFieldsVisibility);
|
|
3605
|
+
els.settingsGithubRuleAction?.addEventListener('change', updateGithubRuleFieldsVisibility);
|
|
3606
|
+
|
|
3607
|
+
els.settingsTabs?.addEventListener('click', (e) => {
|
|
3608
|
+
const btn = e.target.closest('[data-settings-tab]');
|
|
3609
|
+
if (!btn?.dataset.settingsTab) return;
|
|
3610
|
+
setSettingsTab(
|
|
3611
|
+
/** @type {'ticket' | 'auth' | 'rules' | 'config'} */ (btn.dataset.settingsTab)
|
|
3612
|
+
);
|
|
3613
|
+
});
|
|
3614
|
+
|
|
3615
|
+
els.settingsTabs?.addEventListener('keydown', (e) => {
|
|
3616
|
+
const tabs = [...(els.settingsTabs?.querySelectorAll('[role="tab"]') || [])];
|
|
3617
|
+
if (!tabs.length) return;
|
|
3618
|
+
const current = document.activeElement;
|
|
3619
|
+
const idx = tabs.indexOf(/** @type {HTMLElement} */ (current));
|
|
3620
|
+
if (idx < 0) return;
|
|
3621
|
+
let nextIdx = -1;
|
|
3622
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
|
3623
|
+
nextIdx = (idx + 1) % tabs.length;
|
|
3624
|
+
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
|
3625
|
+
nextIdx = (idx - 1 + tabs.length) % tabs.length;
|
|
3626
|
+
} else if (e.key === 'Home') {
|
|
3627
|
+
nextIdx = 0;
|
|
3628
|
+
} else if (e.key === 'End') {
|
|
3629
|
+
nextIdx = tabs.length - 1;
|
|
3630
|
+
}
|
|
3631
|
+
if (nextIdx < 0) return;
|
|
3632
|
+
e.preventDefault();
|
|
3633
|
+
const nextTab = tabs[nextIdx];
|
|
3634
|
+
setSettingsTab(
|
|
3635
|
+
/** @type {'ticket' | 'auth' | 'rules' | 'config'} */ (nextTab.dataset.settingsTab)
|
|
3636
|
+
);
|
|
3637
|
+
nextTab.focus();
|
|
3638
|
+
});
|
|
3639
|
+
|
|
3640
|
+
/**
|
|
3641
|
+
* @param {HTMLInputElement | null} inputEl
|
|
3642
|
+
* @param {HTMLElement | null} hintEl
|
|
3643
|
+
* @param {string} clearedHint
|
|
3644
|
+
*/
|
|
3645
|
+
function bindSecretClear(clearBtn, inputEl, hintEl, clearedHint) {
|
|
3646
|
+
clearBtn?.addEventListener('click', () => {
|
|
3647
|
+
if (!inputEl) return;
|
|
3648
|
+
inputEl.value = '';
|
|
3649
|
+
inputEl.dataset.clear = '1';
|
|
3650
|
+
inputEl.placeholder = 'Will be removed on save';
|
|
3651
|
+
if (hintEl) hintEl.textContent = clearedHint;
|
|
3652
|
+
});
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
bindSecretClear(
|
|
3656
|
+
els.settingsGhTokenClear,
|
|
3657
|
+
els.settingsGhToken,
|
|
3658
|
+
els.settingsGhTokenHint,
|
|
3659
|
+
'Saved GH_TOKEN will be removed when you click Save settings.'
|
|
3660
|
+
);
|
|
3661
|
+
bindSecretClear(
|
|
3662
|
+
els.settingsAnthropicKeyClear,
|
|
3663
|
+
els.settingsAnthropicKey,
|
|
3664
|
+
els.settingsAnthropicKeyHint,
|
|
3665
|
+
'Saved API key will be removed on save so subscription login can be used.'
|
|
3666
|
+
);
|
|
3667
|
+
bindSecretClear(
|
|
3668
|
+
els.settingsClaudeOauthClear,
|
|
3669
|
+
els.settingsClaudeOauth,
|
|
3670
|
+
els.settingsClaudeOauthHint,
|
|
3671
|
+
'Saved CLAUDE_CODE_OAUTH_TOKEN will be removed when you click Save settings.'
|
|
3672
|
+
);
|
|
3298
3673
|
|
|
3299
3674
|
els.addBtn.addEventListener('click', async () => {
|
|
3300
3675
|
els.enqueueFeedback.classList.add('hidden');
|
|
@@ -3615,5 +3990,6 @@ els.logJobFilter.addEventListener('change', () => {
|
|
|
3615
3990
|
|
|
3616
3991
|
applyTheme(loadTheme());
|
|
3617
3992
|
fetchConfig();
|
|
3993
|
+
fetchModels();
|
|
3618
3994
|
fetchJobs();
|
|
3619
3995
|
setInterval(fetchJobs, 3000);
|