@in-the-loop-labs/pair-review 1.0.0
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/LICENSE +674 -0
- package/README.md +371 -0
- package/bin/git-diff-lines +146 -0
- package/bin/pair-review.js +49 -0
- package/package.json +71 -0
- package/public/css/ai-summary-modal.css +183 -0
- package/public/css/pr.css +8698 -0
- package/public/css/repo-settings.css +891 -0
- package/public/css/styles.css +479 -0
- package/public/favicon.png +0 -0
- package/public/index.html +1104 -0
- package/public/js/components/AIPanel.js +1639 -0
- package/public/js/components/AISummaryModal.js +278 -0
- package/public/js/components/AnalysisConfigModal.js +684 -0
- package/public/js/components/ConfirmDialog.js +227 -0
- package/public/js/components/PreviewModal.js +344 -0
- package/public/js/components/ProgressModal.js +678 -0
- package/public/js/components/ReviewModal.js +531 -0
- package/public/js/components/SplitButton.js +382 -0
- package/public/js/components/StatusIndicator.js +265 -0
- package/public/js/components/SuggestionNavigator.js +489 -0
- package/public/js/components/Toast.js +166 -0
- package/public/js/local.js +1580 -0
- package/public/js/modules/analysis-history.js +940 -0
- package/public/js/modules/comment-manager.js +643 -0
- package/public/js/modules/diff-renderer.js +585 -0
- package/public/js/modules/file-comment-manager.js +1242 -0
- package/public/js/modules/gap-coordinates.js +190 -0
- package/public/js/modules/hunk-parser.js +358 -0
- package/public/js/modules/line-tracker.js +386 -0
- package/public/js/modules/panel-resizer.js +228 -0
- package/public/js/modules/storage-cleanup.js +36 -0
- package/public/js/modules/suggestion-manager.js +692 -0
- package/public/js/pr.js +3503 -0
- package/public/js/repo-settings.js +691 -0
- package/public/js/utils/file-order.js +87 -0
- package/public/js/utils/markdown.js +97 -0
- package/public/js/utils/suggestion-ui.js +55 -0
- package/public/js/utils/tier-icons.js +25 -0
- package/public/local.html +460 -0
- package/public/pr.html +329 -0
- package/public/repo-settings.html +243 -0
- package/src/ai/analyzer.js +2592 -0
- package/src/ai/claude-cli.js +153 -0
- package/src/ai/claude-provider.js +261 -0
- package/src/ai/codex-provider.js +361 -0
- package/src/ai/copilot-provider.js +345 -0
- package/src/ai/gemini-provider.js +375 -0
- package/src/ai/index.js +47 -0
- package/src/ai/prompts/baseline/_meta.json +14 -0
- package/src/ai/prompts/baseline/level1/balanced.js +239 -0
- package/src/ai/prompts/baseline/level1/fast.js +194 -0
- package/src/ai/prompts/baseline/level1/thorough.js +319 -0
- package/src/ai/prompts/baseline/level2/balanced.js +248 -0
- package/src/ai/prompts/baseline/level2/fast.js +201 -0
- package/src/ai/prompts/baseline/level2/thorough.js +367 -0
- package/src/ai/prompts/baseline/level3/balanced.js +280 -0
- package/src/ai/prompts/baseline/level3/fast.js +220 -0
- package/src/ai/prompts/baseline/level3/thorough.js +459 -0
- package/src/ai/prompts/baseline/orchestration/balanced.js +259 -0
- package/src/ai/prompts/baseline/orchestration/fast.js +213 -0
- package/src/ai/prompts/baseline/orchestration/thorough.js +446 -0
- package/src/ai/prompts/config.js +52 -0
- package/src/ai/prompts/index.js +267 -0
- package/src/ai/prompts/shared/diff-instructions.js +50 -0
- package/src/ai/prompts/shared/output-schema.js +179 -0
- package/src/ai/prompts/shared/valid-files.js +37 -0
- package/src/ai/provider.js +260 -0
- package/src/config.js +139 -0
- package/src/database.js +2284 -0
- package/src/git/gitattributes.js +207 -0
- package/src/git/worktree.js +688 -0
- package/src/github/client.js +893 -0
- package/src/github/parser.js +247 -0
- package/src/local-review.js +691 -0
- package/src/main.js +987 -0
- package/src/routes/analysis.js +897 -0
- package/src/routes/comments.js +534 -0
- package/src/routes/config.js +250 -0
- package/src/routes/local.js +1728 -0
- package/src/routes/pr.js +1164 -0
- package/src/routes/shared.js +218 -0
- package/src/routes/worktrees.js +500 -0
- package/src/server.js +295 -0
- package/src/utils/diff-annotator.js +414 -0
- package/src/utils/instructions.js +33 -0
- package/src/utils/json-extractor.js +107 -0
- package/src/utils/line-validation.js +183 -0
- package/src/utils/logger.js +142 -0
- package/src/utils/paths.js +161 -0
- package/src/utils/stats-calculator.js +86 -0
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
/**
|
|
3
|
+
* Repository Settings Page JavaScript
|
|
4
|
+
* Handles loading, saving, and managing repository AI settings
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class RepoSettingsPage {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.owner = null;
|
|
10
|
+
this.repo = null;
|
|
11
|
+
this.originalSettings = {};
|
|
12
|
+
this.currentSettings = {};
|
|
13
|
+
this.hasUnsavedChanges = false;
|
|
14
|
+
this.providers = {};
|
|
15
|
+
this.selectedProvider = null;
|
|
16
|
+
|
|
17
|
+
this.init();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async init() {
|
|
21
|
+
// Parse URL to get owner/repo
|
|
22
|
+
this.parseUrl();
|
|
23
|
+
|
|
24
|
+
// Initialize theme
|
|
25
|
+
this.initTheme();
|
|
26
|
+
|
|
27
|
+
// Check for and display back to PR link
|
|
28
|
+
this.initBackLink();
|
|
29
|
+
|
|
30
|
+
// Setup event listeners
|
|
31
|
+
this.setupEventListeners();
|
|
32
|
+
|
|
33
|
+
// Load providers first (needed to render model cards)
|
|
34
|
+
await this.loadProviders();
|
|
35
|
+
|
|
36
|
+
// Load settings
|
|
37
|
+
await this.loadSettings();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initialize back link if user navigated from a PR page or local review
|
|
42
|
+
*/
|
|
43
|
+
initBackLink() {
|
|
44
|
+
const backLink = document.getElementById('back-to-pr');
|
|
45
|
+
const backLinkText = document.getElementById('back-to-pr-text');
|
|
46
|
+
if (!backLink || !backLinkText) return;
|
|
47
|
+
|
|
48
|
+
// Use scoped key to prevent collision between multiple tabs
|
|
49
|
+
const referrerKey = `settingsReferrer:${this.owner}/${this.repo}`;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const referrerData = localStorage.getItem(referrerKey);
|
|
53
|
+
if (!referrerData) return;
|
|
54
|
+
|
|
55
|
+
const data = JSON.parse(referrerData);
|
|
56
|
+
|
|
57
|
+
// Check if this is a local review referrer
|
|
58
|
+
if (data.type === 'local' && data.localReviewId) {
|
|
59
|
+
backLink.href = `/local/${data.localReviewId}`;
|
|
60
|
+
backLinkText.textContent = 'Return to Local Review';
|
|
61
|
+
backLink.style.display = 'inline-flex';
|
|
62
|
+
|
|
63
|
+
// Clear the referrer when clicking the back link
|
|
64
|
+
backLink.addEventListener('click', () => {
|
|
65
|
+
localStorage.removeItem(referrerKey);
|
|
66
|
+
});
|
|
67
|
+
} else if (data.prNumber) {
|
|
68
|
+
// PR referrer - validate stored data matches current page context as sanity check
|
|
69
|
+
// (Key is already scoped by repo, but this provides extra safety)
|
|
70
|
+
if (data.owner && data.repo && (data.owner !== this.owner || data.repo !== this.repo)) {
|
|
71
|
+
console.warn('PR referrer owner/repo mismatch - clearing stale data');
|
|
72
|
+
localStorage.removeItem(referrerKey);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
backLink.href = `/pr/${this.owner}/${this.repo}/${data.prNumber}`;
|
|
76
|
+
backLinkText.textContent = `Return to PR #${data.prNumber}`;
|
|
77
|
+
backLink.style.display = 'inline-flex';
|
|
78
|
+
|
|
79
|
+
// Clear the referrer when clicking the back link
|
|
80
|
+
backLink.addEventListener('click', () => {
|
|
81
|
+
localStorage.removeItem(referrerKey);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// No else clause needed - if format is unknown, just don't show the link
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.warn('Error parsing settings referrer:', error);
|
|
87
|
+
localStorage.removeItem(referrerKey);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
parseUrl() {
|
|
92
|
+
// URL format: /settings/:owner/:repo
|
|
93
|
+
const pathParts = window.location.pathname.split('/').filter(Boolean);
|
|
94
|
+
if (pathParts.length >= 3 && pathParts[0] === 'settings') {
|
|
95
|
+
this.owner = pathParts[1];
|
|
96
|
+
this.repo = pathParts[2];
|
|
97
|
+
} else {
|
|
98
|
+
// Try query params as fallback
|
|
99
|
+
const params = new URLSearchParams(window.location.search);
|
|
100
|
+
this.owner = params.get('owner');
|
|
101
|
+
this.repo = params.get('repo');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!this.owner || !this.repo) {
|
|
105
|
+
this.showToast('error', 'Invalid repository URL');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Update UI with repo name
|
|
110
|
+
const repoFullName = `${this.owner}/${this.repo}`;
|
|
111
|
+
document.getElementById('repo-name-breadcrumb').textContent = repoFullName;
|
|
112
|
+
document.getElementById('repo-name-header').textContent = repoFullName;
|
|
113
|
+
document.title = `Settings - ${repoFullName} - Pair Review`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
initTheme() {
|
|
117
|
+
// Check for saved theme preference
|
|
118
|
+
const savedTheme = localStorage.getItem('theme') || 'light';
|
|
119
|
+
document.documentElement.setAttribute('data-theme', savedTheme);
|
|
120
|
+
|
|
121
|
+
// Theme toggle button
|
|
122
|
+
const themeToggle = document.getElementById('theme-toggle');
|
|
123
|
+
if (themeToggle) {
|
|
124
|
+
themeToggle.addEventListener('click', () => {
|
|
125
|
+
const currentTheme = document.documentElement.getAttribute('data-theme');
|
|
126
|
+
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
127
|
+
document.documentElement.setAttribute('data-theme', newTheme);
|
|
128
|
+
localStorage.setItem('theme', newTheme);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
setupEventListeners() {
|
|
134
|
+
// Model card and provider button listeners are attached dynamically
|
|
135
|
+
// when renderProviderButtons() and renderModelCards() are called
|
|
136
|
+
|
|
137
|
+
// Instructions textarea
|
|
138
|
+
const textarea = document.getElementById('default-instructions');
|
|
139
|
+
if (textarea) {
|
|
140
|
+
textarea.addEventListener('input', () => {
|
|
141
|
+
this.currentSettings.default_instructions = textarea.value;
|
|
142
|
+
this.updateCharCount(textarea.value.length);
|
|
143
|
+
this.checkForChanges();
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Form submission
|
|
148
|
+
const form = document.getElementById('settings-form');
|
|
149
|
+
if (form) {
|
|
150
|
+
form.addEventListener('submit', (e) => {
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
this.saveSettings();
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Cancel button
|
|
157
|
+
const cancelBtn = document.getElementById('cancel-btn');
|
|
158
|
+
if (cancelBtn) {
|
|
159
|
+
cancelBtn.addEventListener('click', () => this.handleCancel());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Reset settings button
|
|
163
|
+
const resetBtn = document.getElementById('reset-settings');
|
|
164
|
+
if (resetBtn) {
|
|
165
|
+
resetBtn.addEventListener('click', () => this.handleReset());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Clear local path button
|
|
169
|
+
const clearLocalPathBtn = document.getElementById('clear-local-path');
|
|
170
|
+
if (clearLocalPathBtn) {
|
|
171
|
+
clearLocalPathBtn.addEventListener('click', () => this.handleClearLocalPath());
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Warn before leaving with unsaved changes
|
|
175
|
+
window.addEventListener('beforeunload', (e) => {
|
|
176
|
+
if (this.hasUnsavedChanges) {
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
e.returnValue = '';
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Load provider definitions from the backend API
|
|
185
|
+
*/
|
|
186
|
+
async loadProviders() {
|
|
187
|
+
try {
|
|
188
|
+
const response = await fetch('/api/providers');
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
throw new Error('Failed to fetch providers');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const data = await response.json();
|
|
194
|
+
|
|
195
|
+
// Convert array to object keyed by provider id
|
|
196
|
+
this.providers = {};
|
|
197
|
+
for (const provider of data.providers) {
|
|
198
|
+
this.providers[provider.id] = provider;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Render provider buttons now that we have data
|
|
202
|
+
this.renderProviderButtons();
|
|
203
|
+
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error('Error loading providers:', error);
|
|
206
|
+
// Last-resort degraded mode: hardcoded Claude fallback when the /api/providers
|
|
207
|
+
// endpoint is unavailable. This allows basic functionality even if the backend
|
|
208
|
+
// is partially broken. The canonical provider definitions live in
|
|
209
|
+
// src/ai/claude-provider.js - this fallback should mirror those values.
|
|
210
|
+
this.providers = {
|
|
211
|
+
claude: {
|
|
212
|
+
id: 'claude',
|
|
213
|
+
name: 'Claude',
|
|
214
|
+
models: [
|
|
215
|
+
{ id: 'haiku', name: 'Haiku', tier: 'fast', badge: 'Fastest', badgeClass: 'badge-speed', tagline: 'Lightning Fast', description: 'Quick analysis for simple changes' },
|
|
216
|
+
{ id: 'sonnet', name: 'Sonnet', tier: 'balanced', default: true, badge: 'Recommended', badgeClass: 'badge-recommended', tagline: 'Best Balance', description: 'Recommended for most reviews' },
|
|
217
|
+
{ id: 'opus', name: 'Opus', tier: 'thorough', badge: 'Most Thorough', badgeClass: 'badge-power', tagline: 'Most Capable', description: 'Deep analysis for complex code' }
|
|
218
|
+
],
|
|
219
|
+
defaultModel: 'sonnet'
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
this.renderProviderButtons();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Render provider toggle buttons
|
|
228
|
+
*/
|
|
229
|
+
renderProviderButtons() {
|
|
230
|
+
const container = document.getElementById('provider-toggle');
|
|
231
|
+
if (!container) return;
|
|
232
|
+
|
|
233
|
+
const providerIds = Object.keys(this.providers);
|
|
234
|
+
container.innerHTML = providerIds.map(providerId => {
|
|
235
|
+
const provider = this.providers[providerId];
|
|
236
|
+
return `
|
|
237
|
+
<button type="button" class="provider-btn ${providerId === this.selectedProvider ? 'selected' : ''}" data-provider="${providerId}">
|
|
238
|
+
${provider.name}
|
|
239
|
+
</button>
|
|
240
|
+
`;
|
|
241
|
+
}).join('');
|
|
242
|
+
|
|
243
|
+
// Attach event listeners
|
|
244
|
+
container.querySelectorAll('.provider-btn').forEach(btn => {
|
|
245
|
+
btn.addEventListener('click', () => this.selectProvider(btn.dataset.provider));
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Select a provider and update model cards
|
|
251
|
+
*/
|
|
252
|
+
selectProvider(providerId, markAsChanged = true) {
|
|
253
|
+
if (!this.providers[providerId]) return;
|
|
254
|
+
|
|
255
|
+
const previousProvider = this.selectedProvider;
|
|
256
|
+
this.selectedProvider = providerId;
|
|
257
|
+
|
|
258
|
+
if (markAsChanged) {
|
|
259
|
+
this.currentSettings.default_provider = providerId;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Update provider buttons UI
|
|
263
|
+
document.querySelectorAll('.provider-btn').forEach(btn => {
|
|
264
|
+
btn.classList.toggle('selected', btn.dataset.provider === providerId);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Re-render model cards for the new provider
|
|
268
|
+
this.renderModelCards();
|
|
269
|
+
|
|
270
|
+
// If provider changed and we're tracking changes, try to map the model to the new provider
|
|
271
|
+
if (markAsChanged && previousProvider && previousProvider !== providerId) {
|
|
272
|
+
const oldProvider = this.providers[previousProvider];
|
|
273
|
+
const newProvider = this.providers[providerId];
|
|
274
|
+
|
|
275
|
+
// If old provider no longer exists (e.g., was removed from available providers),
|
|
276
|
+
// fall back to the new provider's default model
|
|
277
|
+
if (!oldProvider) {
|
|
278
|
+
const defaultModel = newProvider.models.find(m => m.default) || newProvider.models[0];
|
|
279
|
+
this.selectModel(defaultModel.id, markAsChanged);
|
|
280
|
+
this.checkForChanges();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Find the model with same tier as currently selected
|
|
285
|
+
const currentModel = oldProvider.models.find(m => m.id === this.currentSettings.default_model);
|
|
286
|
+
|
|
287
|
+
if (currentModel) {
|
|
288
|
+
const matchingModel = newProvider.models.find(m => m.tier === currentModel.tier);
|
|
289
|
+
const defaultModel = newProvider.models.find(m => m.default);
|
|
290
|
+
const fallbackModel = matchingModel || defaultModel || newProvider.models[0];
|
|
291
|
+
|
|
292
|
+
if (!matchingModel && !defaultModel) {
|
|
293
|
+
console.warn(`No matching tier or default model found for provider "${providerId}", using first available model`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
this.selectModel(fallbackModel.id, markAsChanged);
|
|
297
|
+
} else {
|
|
298
|
+
// No current model selected, use default for new provider
|
|
299
|
+
const defaultModel = newProvider.models.find(m => m.default) || newProvider.models[0];
|
|
300
|
+
this.selectModel(defaultModel.id, markAsChanged);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
this.checkForChanges();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Render model cards for the currently selected provider
|
|
309
|
+
*/
|
|
310
|
+
renderModelCards() {
|
|
311
|
+
const container = document.getElementById('model-cards');
|
|
312
|
+
if (!container) return;
|
|
313
|
+
|
|
314
|
+
const provider = this.providers[this.selectedProvider];
|
|
315
|
+
if (!provider) {
|
|
316
|
+
container.innerHTML = '';
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
container.innerHTML = provider.models.map(model => `
|
|
321
|
+
<button type="button" class="model-card ${model.id === this.currentSettings.default_model ? 'selected' : ''}" data-model="${model.id}" data-tier="${model.tier}">
|
|
322
|
+
<div class="model-badge ${model.badgeClass || ''}">${model.badge || ''}</div>
|
|
323
|
+
<div class="model-icon">${this.getModelIcon(model.tier)}</div>
|
|
324
|
+
<div class="model-info">
|
|
325
|
+
<span class="model-name">${model.name}</span>
|
|
326
|
+
<span class="model-tagline">${model.tagline || ''}</span>
|
|
327
|
+
</div>
|
|
328
|
+
<p class="model-description">${model.description || ''}</p>
|
|
329
|
+
<div class="model-selected-indicator">
|
|
330
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
331
|
+
<path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
|
|
332
|
+
</svg>
|
|
333
|
+
</div>
|
|
334
|
+
</button>
|
|
335
|
+
`).join('');
|
|
336
|
+
|
|
337
|
+
// Attach event listeners
|
|
338
|
+
container.querySelectorAll('.model-card').forEach(card => {
|
|
339
|
+
card.addEventListener('click', () => this.selectModel(card.dataset.model));
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Get model icon based on tier
|
|
345
|
+
* Delegates to shared utility in utils/tier-icons.js
|
|
346
|
+
*/
|
|
347
|
+
getModelIcon(tier) {
|
|
348
|
+
return window.getTierIcon(tier);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async loadSettings() {
|
|
352
|
+
if (!this.owner || !this.repo) return;
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const response = await fetch(`/api/repos/${this.owner}/${this.repo}/settings`);
|
|
356
|
+
|
|
357
|
+
if (!response.ok) {
|
|
358
|
+
throw new Error('Failed to load settings');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const settings = await response.json();
|
|
362
|
+
|
|
363
|
+
// Store original settings for comparison
|
|
364
|
+
this.originalSettings = {
|
|
365
|
+
default_provider: settings.default_provider || null,
|
|
366
|
+
default_model: settings.default_model || null,
|
|
367
|
+
default_instructions: settings.default_instructions || '',
|
|
368
|
+
local_path: settings.local_path || null
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// Set current settings
|
|
372
|
+
this.currentSettings = { ...this.originalSettings };
|
|
373
|
+
|
|
374
|
+
// Update UI
|
|
375
|
+
this.updateUI();
|
|
376
|
+
|
|
377
|
+
} catch (error) {
|
|
378
|
+
console.error('Error loading settings:', error);
|
|
379
|
+
// Use defaults if no settings exist
|
|
380
|
+
this.originalSettings = {
|
|
381
|
+
default_provider: null,
|
|
382
|
+
default_model: null,
|
|
383
|
+
default_instructions: '',
|
|
384
|
+
local_path: null
|
|
385
|
+
};
|
|
386
|
+
this.currentSettings = { ...this.originalSettings };
|
|
387
|
+
this.updateUI();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
updateUI() {
|
|
392
|
+
// Update provider selection - validate provider exists before selecting
|
|
393
|
+
let providerId = this.currentSettings.default_provider;
|
|
394
|
+
const availableProviders = Object.keys(this.providers);
|
|
395
|
+
|
|
396
|
+
if (!providerId || !this.providers[providerId]) {
|
|
397
|
+
// Provider doesn't exist, fall back to first available
|
|
398
|
+
// Update currentSettings directly so state is consistent for saves,
|
|
399
|
+
// but don't mark as changed (no unsaved changes indicator)
|
|
400
|
+
providerId = availableProviders[0] || 'claude';
|
|
401
|
+
this.currentSettings.default_provider = providerId;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
this.selectProvider(providerId, false);
|
|
405
|
+
|
|
406
|
+
// Update model selection
|
|
407
|
+
if (this.currentSettings.default_model) {
|
|
408
|
+
this.selectModel(this.currentSettings.default_model, false);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Update instructions textarea
|
|
412
|
+
const textarea = document.getElementById('default-instructions');
|
|
413
|
+
if (textarea) {
|
|
414
|
+
textarea.value = this.currentSettings.default_instructions || '';
|
|
415
|
+
this.updateCharCount(textarea.value.length);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Update local path display
|
|
419
|
+
this.updateLocalPathDisplay();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Update the local path display section
|
|
424
|
+
*/
|
|
425
|
+
updateLocalPathDisplay() {
|
|
426
|
+
const localPathValue = document.getElementById('local-path-value');
|
|
427
|
+
const clearLocalPathBtn = document.getElementById('clear-local-path');
|
|
428
|
+
const localPathHint = document.getElementById('local-path-hint');
|
|
429
|
+
|
|
430
|
+
if (!localPathValue) return;
|
|
431
|
+
|
|
432
|
+
const localPath = this.currentSettings.local_path;
|
|
433
|
+
|
|
434
|
+
if (localPath) {
|
|
435
|
+
localPathValue.textContent = localPath;
|
|
436
|
+
localPathValue.classList.add('has-value');
|
|
437
|
+
if (clearLocalPathBtn) clearLocalPathBtn.style.display = 'inline-flex';
|
|
438
|
+
if (localPathHint) localPathHint.style.display = 'none';
|
|
439
|
+
} else {
|
|
440
|
+
localPathValue.textContent = 'Not set';
|
|
441
|
+
localPathValue.classList.remove('has-value');
|
|
442
|
+
if (clearLocalPathBtn) clearLocalPathBtn.style.display = 'none';
|
|
443
|
+
if (localPathHint) localPathHint.style.display = 'flex';
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
selectModel(modelId, markAsChanged = true) {
|
|
448
|
+
// Update current settings
|
|
449
|
+
if (markAsChanged) {
|
|
450
|
+
this.currentSettings.default_model = modelId;
|
|
451
|
+
this.checkForChanges();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Update UI
|
|
455
|
+
document.querySelectorAll('.model-card').forEach(card => {
|
|
456
|
+
card.classList.toggle('selected', card.dataset.model === modelId);
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
updateCharCount(count) {
|
|
461
|
+
const charCountEl = document.getElementById('char-count');
|
|
462
|
+
if (charCountEl) {
|
|
463
|
+
charCountEl.textContent = count;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
checkForChanges() {
|
|
468
|
+
// Use nullish coalescing to normalize null/undefined for consistent comparison
|
|
469
|
+
const providerChanged = (this.currentSettings.default_provider ?? null) !== (this.originalSettings.default_provider ?? null);
|
|
470
|
+
const modelChanged = (this.currentSettings.default_model ?? null) !== (this.originalSettings.default_model ?? null);
|
|
471
|
+
const instructionsChanged = (this.currentSettings.default_instructions ?? '') !== (this.originalSettings.default_instructions ?? '');
|
|
472
|
+
|
|
473
|
+
this.hasUnsavedChanges = providerChanged || modelChanged || instructionsChanged;
|
|
474
|
+
|
|
475
|
+
// Show/hide action bar
|
|
476
|
+
const actionBar = document.getElementById('action-bar');
|
|
477
|
+
if (actionBar) {
|
|
478
|
+
actionBar.classList.toggle('visible', this.hasUnsavedChanges);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async saveSettings() {
|
|
483
|
+
if (!this.owner || !this.repo) return;
|
|
484
|
+
|
|
485
|
+
const saveBtn = document.getElementById('save-btn');
|
|
486
|
+
if (saveBtn) {
|
|
487
|
+
saveBtn.disabled = true;
|
|
488
|
+
saveBtn.innerHTML = `
|
|
489
|
+
<svg class="spinner" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
490
|
+
<path d="M8 0a8 8 0 018 8h-2a6 6 0 00-6-6V0z"/>
|
|
491
|
+
</svg>
|
|
492
|
+
Saving...
|
|
493
|
+
`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
const response = await fetch(`/api/repos/${this.owner}/${this.repo}/settings`, {
|
|
498
|
+
method: 'POST',
|
|
499
|
+
headers: {
|
|
500
|
+
'Content-Type': 'application/json'
|
|
501
|
+
},
|
|
502
|
+
body: JSON.stringify({
|
|
503
|
+
default_provider: this.currentSettings.default_provider,
|
|
504
|
+
default_model: this.currentSettings.default_model,
|
|
505
|
+
default_instructions: this.currentSettings.default_instructions
|
|
506
|
+
})
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
if (!response.ok) {
|
|
510
|
+
throw new Error('Failed to save settings');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Update original settings
|
|
514
|
+
this.originalSettings = { ...this.currentSettings };
|
|
515
|
+
this.hasUnsavedChanges = false;
|
|
516
|
+
|
|
517
|
+
// Hide action bar
|
|
518
|
+
const actionBar = document.getElementById('action-bar');
|
|
519
|
+
if (actionBar) {
|
|
520
|
+
actionBar.classList.remove('visible');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
this.showToast('success', 'Settings saved successfully');
|
|
524
|
+
|
|
525
|
+
} catch (error) {
|
|
526
|
+
console.error('Error saving settings:', error);
|
|
527
|
+
this.showToast('error', 'Failed to save settings. Please try again.');
|
|
528
|
+
} finally {
|
|
529
|
+
if (saveBtn) {
|
|
530
|
+
saveBtn.disabled = false;
|
|
531
|
+
saveBtn.innerHTML = `
|
|
532
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
533
|
+
<path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
|
|
534
|
+
</svg>
|
|
535
|
+
Save Settings
|
|
536
|
+
`;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
handleCancel() {
|
|
542
|
+
if (this.hasUnsavedChanges) {
|
|
543
|
+
const confirmed = confirm('You have unsaved changes. Are you sure you want to discard them?');
|
|
544
|
+
if (!confirmed) return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Reset to original settings
|
|
548
|
+
this.currentSettings = { ...this.originalSettings };
|
|
549
|
+
this.hasUnsavedChanges = false;
|
|
550
|
+
this.updateUI();
|
|
551
|
+
|
|
552
|
+
// Hide action bar
|
|
553
|
+
const actionBar = document.getElementById('action-bar');
|
|
554
|
+
if (actionBar) {
|
|
555
|
+
actionBar.classList.remove('visible');
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async handleReset() {
|
|
560
|
+
const confirmed = confirm(
|
|
561
|
+
'This will remove all custom settings for this repository. The default provider and model will not be pre-selected and no default instructions will be used. Continue?'
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
if (!confirmed) return;
|
|
565
|
+
|
|
566
|
+
try {
|
|
567
|
+
// For now, just clear the settings by saving empty values
|
|
568
|
+
const response = await fetch(`/api/repos/${this.owner}/${this.repo}/settings`, {
|
|
569
|
+
method: 'POST',
|
|
570
|
+
headers: {
|
|
571
|
+
'Content-Type': 'application/json'
|
|
572
|
+
},
|
|
573
|
+
body: JSON.stringify({
|
|
574
|
+
default_provider: null,
|
|
575
|
+
default_model: null,
|
|
576
|
+
default_instructions: '',
|
|
577
|
+
local_path: null
|
|
578
|
+
})
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
if (!response.ok) {
|
|
582
|
+
throw new Error('Failed to reset settings');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Clear all settings
|
|
586
|
+
this.originalSettings = {
|
|
587
|
+
default_provider: null,
|
|
588
|
+
default_model: null,
|
|
589
|
+
default_instructions: '',
|
|
590
|
+
local_path: null
|
|
591
|
+
};
|
|
592
|
+
this.currentSettings = { ...this.originalSettings };
|
|
593
|
+
this.hasUnsavedChanges = false;
|
|
594
|
+
|
|
595
|
+
// Update UI using updateUI() which handles all the display logic
|
|
596
|
+
this.updateUI();
|
|
597
|
+
|
|
598
|
+
// Hide action bar
|
|
599
|
+
const actionBar = document.getElementById('action-bar');
|
|
600
|
+
if (actionBar) {
|
|
601
|
+
actionBar.classList.remove('visible');
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
this.showToast('success', 'Settings reset to defaults');
|
|
605
|
+
|
|
606
|
+
} catch (error) {
|
|
607
|
+
console.error('Error resetting settings:', error);
|
|
608
|
+
this.showToast('error', 'Failed to reset settings. Please try again.');
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Handle clearing the local repository path
|
|
614
|
+
*/
|
|
615
|
+
async handleClearLocalPath() {
|
|
616
|
+
const confirmed = confirm(
|
|
617
|
+
'This will clear the registered repository location. The next web UI review will need to clone the repository unless you run pair-review from the CLI again. Continue?'
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
if (!confirmed) return;
|
|
621
|
+
|
|
622
|
+
try {
|
|
623
|
+
const response = await fetch(`/api/repos/${this.owner}/${this.repo}/settings`, {
|
|
624
|
+
method: 'POST',
|
|
625
|
+
headers: {
|
|
626
|
+
'Content-Type': 'application/json'
|
|
627
|
+
},
|
|
628
|
+
body: JSON.stringify({
|
|
629
|
+
local_path: null
|
|
630
|
+
})
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
if (!response.ok) {
|
|
634
|
+
throw new Error('Failed to clear local path');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Update settings
|
|
638
|
+
this.originalSettings.local_path = null;
|
|
639
|
+
this.currentSettings.local_path = null;
|
|
640
|
+
|
|
641
|
+
// Update local path display
|
|
642
|
+
this.updateLocalPathDisplay();
|
|
643
|
+
|
|
644
|
+
this.showToast('success', 'Repository location cleared');
|
|
645
|
+
|
|
646
|
+
} catch (error) {
|
|
647
|
+
console.error('Error clearing local path:', error);
|
|
648
|
+
this.showToast('error', 'Failed to clear repository location. Please try again.');
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
showToast(type, message) {
|
|
653
|
+
const container = document.getElementById('toast-container');
|
|
654
|
+
if (!container) return;
|
|
655
|
+
|
|
656
|
+
const toast = document.createElement('div');
|
|
657
|
+
toast.className = `toast ${type}`;
|
|
658
|
+
toast.innerHTML = `
|
|
659
|
+
<div class="toast-icon">
|
|
660
|
+
${type === 'success'
|
|
661
|
+
? '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/></svg>'
|
|
662
|
+
: '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9-3a1 1 0 11-2 0 1 1 0 012 0zM6.92 6.085c.081-.16.19-.299.34-.398.145-.097.371-.187.74-.187.302 0 .558.066.743.205a.677.677 0 01.26.514c0 .217-.062.376-.15.495-.083.112-.216.22-.436.345-.205.119-.36.234-.479.364a.788.788 0 00-.19.478v.413a.75.75 0 101.5 0v-.124c0-.05.024-.1.067-.14.052-.047.154-.113.333-.19.188-.083.37-.196.523-.35a1.724 1.724 0 00.437-1.18c0-.515-.177-.914-.504-1.199-.331-.289-.78-.447-1.3-.447-.531 0-.978.164-1.307.465-.323.295-.496.682-.558 1.093a.75.75 0 001.474.28.327.327 0 01.029-.073zM9 11a1 1 0 11-2 0 1 1 0 012 0z"/></svg>'
|
|
663
|
+
}
|
|
664
|
+
</div>
|
|
665
|
+
<span class="toast-message">${message}</span>
|
|
666
|
+
<button class="toast-close" onclick="this.parentElement.remove()">
|
|
667
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
|
668
|
+
<path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"/>
|
|
669
|
+
</svg>
|
|
670
|
+
</button>
|
|
671
|
+
`;
|
|
672
|
+
|
|
673
|
+
container.appendChild(toast);
|
|
674
|
+
|
|
675
|
+
// Trigger animation
|
|
676
|
+
requestAnimationFrame(() => {
|
|
677
|
+
toast.classList.add('show');
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// Auto-remove after 5 seconds
|
|
681
|
+
setTimeout(() => {
|
|
682
|
+
toast.classList.remove('show');
|
|
683
|
+
setTimeout(() => toast.remove(), 300);
|
|
684
|
+
}, 5000);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Initialize when DOM is ready
|
|
689
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
690
|
+
window.repoSettings = new RepoSettingsPage();
|
|
691
|
+
});
|